Q:

OOP implementation of Blackjack in Java - part 2

Part 1
Last time I got a lot of great advice, so this is take two with all that in mind:
My main concern with this implementation is the use of Inheritance (A base Player) vs having that as a interface - as that would lead to duplicate code in both Player and Dealer classes. 
Everything works - the last thing for me to implement is having the dealer try to get to 21 after all the players. 
But I would still really appreciate a review of this new version.
Deck.java
package com.tn.deck;

public interface Deck<T> {
    T dealCard();
    void shuffle();
}

AbstractPlayer.java
package com.tn.blackjack;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class AbstractPlayer {
    private static final int WINNING_NUMBER = 21;
    List<Card> hand = new ArrayList<>();

    private boolean containsAce() {
        return hand.stream().anyMatch(card -> card.getRank() == Rank.ACE);
    }

    final void drawCards(Card... cards) {
        hand.addAll(Arrays.asList(cards));
    }

    final boolean isBust() {
        return calculateScore() > WINNING_NUMBER;
    }

    final boolean hasBlackjack() {
        return calculateScore() == WINNING_NUMBER;
    }

    final int calculateScore() {
        int score = hand.stream().mapToInt(card -> card.getRank().getValue()).sum();
        return score > WINNING_NUMBER && containsAce() ?
                score - 10 : //Takes care of ace being either 1 or 11
                score;
    }
}

Card.java
package com.tn.blackjack;

public class Card {
    private final Suit suit;
    private final Rank rank;

    Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
    }

    public Suit getSuit() {
        return suit;
    }

    public Rank getRank() {
        return rank;
    }

    public void print() {
        System.out.printf("%s%s ", suit.getIcon(), rank.getName());
    }
}

CardDeck.java
package com.tn.blackjack;

import com.tn.deck.Deck;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class CardDeck implements Deck<Card> {
    private List<Card> deck;

    CardDeck() {
        this.deck = initializeDeck(Suit.values(), Rank.values());
        shuffle();
    }

    CardDeck(int numberOfDecks) {
        this.deck = initializeMultipleDecks(numberOfDecks, Suit.values(), Rank.values());
        shuffle();
    }

    private List<Card> initializeDeck(Suit[] suits, Rank[] ranks) {
        return Arrays.stream(suits)
                .flatMap(suit -> Arrays.stream(ranks).map(rank -> new Card(suit, rank)))
                .collect(Collectors.toList());
    }

    private List<Card> initializeMultipleDecks(int numberOfDecks, Suit[] suits, Rank[] ranks) {
        return IntStream.range(0, numberOfDecks)
                .mapToObj(i -> initializeDeck(suits, ranks))
                .flatMap(Collection::stream)
                .collect(Collectors.toList());
    }

    @Override
    public Card dealCard() {
        if(deck.size() < 1) {
            throw new IllegalStateException("Deck is empty");
        }
        Card card = deck.get(0);
        deck.remove(0);
        return card;
    }

    @Override
    public void shuffle() {
        Collections.shuffle(deck);
    }
}

Dealer.java
package com.tn.blackjack;

import java.util.Arrays;

public class Dealer extends AbstractPlayer {
    private Prompter prompter = new Prompter();
    private CardDeck deck;

    Dealer() {
        initializeDeck();
    }

    public void dealInitialTwoCards(Player[] players) {
        Arrays.stream(players).forEach(player -> player.drawCards(deck.dealCard(), deck.dealCard()));
        drawCards(deck.dealCard(), deck.dealCard());
    }

    public void startPlayerLoop(Player[] players) {
        Arrays.stream(players).forEach(player -> player.performAction(deck.dealCard()));
    }

    private void initializeDeck() {
        int numberOfDecks = prompter.ask("How many decks should be used? ");
        if(numberOfDecks < 1) {
            throw new IllegalArgumentException("Deck size must be at least 1");
        }

        this.deck = numberOfDecks > 1 ?
                new CardDeck(numberOfDecks) :
                new CardDeck();
    }

}

Player.java
package com.tn.blackjack;

public class Player extends AbstractPlayer {
    private Prompter prompter = new Prompter();
    private int id;

    Player(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void performAction(Card card) {
        System.out.printf("%n%n==== Player %d ====%n%n", getId());
        State state;
        do {
            prompter.printStatus(this);
            state = prompter.getState();
            switch (state) {
                case HIT: drawCards(card);
                    break;
                default:
                    break;
            }
            if(isBust() || hasBlackjack()) {
                prompter.printStatus(this);
                break;
            }

        } while(state == State.HIT);
    }
}

Prompter.java
package com.tn.blackjack;

import java.util.Scanner;

public class Prompter {
    private Scanner scanner = new Scanner(System.in);

    public void printStatus(AbstractPlayer player) {
        player.hand.forEach(Card::print);
        System.out.printf("( score of %d )", player.calculateScore());
        System.out.printf(player.hasBlackjack() ?
                "\tBLACKJACK" : player.isBust() ?
                "\tBUST" : "");
    }

    public State getState() {
        String answer;
        do {
            System.out.printf("%n%nDo you want to (H)it or (S)tand? ");
            answer = scanner.nextLine().trim().toUpperCase();
        } while (!answer.equals("H") && !answer.equals("S"));

        State state;
        switch (answer) {
            case "H": state = State.HIT;
                    break;
            case "S": state = State.STAND;
                    break;
            default: state = null;
        }
        return state;
    }

    public int ask(String question) {
        String answer;
        do {
            System.out.printf("%s", question);
            answer = scanner.nextLine().trim();
        } while (!isInt(answer));
        return Integer.parseInt(answer);
    }

    private static boolean isInt(String s) {
        try {
            Integer.parseInt(s);
        } catch (NumberFormatException ex) {
            return false;
        }
        return true;
    }
}

Rank.java
package com.tn.blackjack;

public enum Rank {
    TWO("2", 2), THREE("3", 3), FOUR("4", 4), FIVE("5", 5),
    SIX("6", 6), SEVEN("7", 7), EIGHT("8", 8), NINE("9", 9), TEN("10", 10),
    JACK("J", 10), QUEEN("Q", 10), KING("K", 10), ACE("A", 11);

    private final String name;
    private final int value;

    Rank(String name, int value) {
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public int getValue() {
        return value;
    }
}

State.java
package com.tn.blackjack;

public enum State {
    HIT, STAND
}

Suit.java
package com.tn.blackjack;

public enum Suit {
    SPADE("\u2660"),
    HEART("\u2665"),
    DIAMOND("\u2666"),
    CLUB("\u2663");

    private final String icon;

    Suit(String icon) {
        this.icon = icon;
    }

    public String getIcon() {
        return icon;
    }
}

Game.java
package com.tn.blackjack;

import java.util.stream.IntStream;

public class Game {
    private Prompter prompter = new Prompter();
    private Dealer dealer;
    private Player[] players;

    public Game() {
        initializePlayers();
    }

    public void start() {
        dealer.dealInitialTwoCards(players);
        dealer.startPlayerLoop(players);
    }

    private void initializePlayers() {
        int numberOfPlayers = prompter.ask("Not including the dealer - How many players? ");
        this.dealer = new Dealer();
        this.players = IntStream.rangeClosed(1, numberOfPlayers)
                .mapToObj(Player::new)
                .toArray(Player[]::new);
    }
}

A:

Guess who has a flash back?
Smaller things:

Deck: I can't see any declaration of that type.
Deck.shuffle(): Is only called within the implementation, so no need to make that public.
AbstractPlayer: is not abstract
AbstractPlayer: The hand is not private (it is implementation of abstraction, and should not be accessible to implementation of the abstraction... if that makes any sense)
The Card itself should be displayed by the Prompter, too (think of adding a GUI)
CardDeck: The Constructor has code duplication. The upper one should call the lower one with 1 as parameter. So initializeMultipleDecks will be initializeDecks, and initializeDeck can be removed. Then, you also get rid of the duplicate shuffle() call.
CardDeck: Not sure if you have to pass the Suit[] and Rank[] array for the initializer methods. I think it's okay, to get the values from the enum itself. (If you want to enhance it later: Add the method with the two parameters again, and delegate the call from the no-param method to the newly added one).
CardDeck.dealCard: There's a 'isEmpty' method in List. And: I can't find the place, where you actually check, if there's no card left? So, you will always end with an exception.
CardDeck.dealCard: return deck.remove(0) does the job.
Player.performAction: The method name doesn't tell me, what the method should do - but I see, this one is a bit tricky.
Player.performAction: I think the loop can be simplified: Can't the if condition moved into the while condition? Also: You have an empty default case.
Prompter.getState: State is not clear. The "H" and "S" is duplicated, that should be a constant with a proper name - maybe even provided in the enum itself (String symbol as constructor of the enum?). Also: the default case will return null. Since the code won't ever reach that point, I'd throw an Exception - if that happens, a bug has been introduced.
Prompter.ask: Who asks what whom why?
Rank: Can't remember, if I have ever seen a non-private constructor in an enum and I didn't know you can actually do that. However, it doesn't make any sense, since noone else can call it anyway?

Inheritance
A main object oriented principle is "lose coupling", inheritance can be, as I think it is in your case, the opposite. The concrete implementations are tightly coupled to its super class. My main problem with that, you can neither test the abstraction without a implementation, nor can you test a implementation without its abstraction.
Further, you're violating Liskov's substitution law (The L in SOLID): If your program works with type T, it should work with U extends T interchangeably, without changing your application. I think it's enough to explicitly declare a Dealer and a Player in that case. I hope someone corrects me, if I'm wrong, or describes it better: I always had a hard time understand the L and I usually have other reasons to avoid inheritance.
Another general rule is composition over inheritance. Meaning: Do not use inheritance for code reuse. It's much more flexible, easier to maintain and what not.
Hope that helps...

