Ist eine unveränderliche Java-Klasse immer endgültig?

Als Antwort auf meinen kürzlich veröffentlichten Blogbeitrag "Unveränderliche Java-Objekte" brachte Matt einen guten Diskussionspunkt in Bezug darauf, Java-Klassen wirklich unveränderlich zu machen, zur Sprache, indem er sie als endgültig deklarierte, damit sie nicht erweitert werden können. Sein Kommentar war:

"Implementierungsvererbung ausdrücklich verboten." - Ist das nicht weitgehend orthogonal zur Frage der Unveränderlichkeit?

In diesem zuvor erwähnten Blog-Beitrag habe ich verschiedene Ressourcen aufgelistet, die erklären, wie unveränderliche Java-Klassen geschrieben werden. Jede dieser Ressourcen empfiehlt, die Java-Klasse als einen der Schritte zur Unveränderlichkeit endgültig zu machen. Zu den Ressourcen mit der Empfehlung gehören Brian Goetzs "Mutieren oder nicht mutieren?", Der Java-Tutorial-Abschnitt "Eine Strategie zum Entwerfen unveränderlicher Objekte" und Joshua Blochs effektives Java.

Eine ähnliche Frage wird in der unveränderlichen Klasse des StackOverflow-Threads gestellt. Sollte endgültig sein?: "Warum ist Unveränderlichkeit ein guter Grund, eine Klasse endgültig zu machen?" In diesem Blog-Beitrag beschäftige ich mich etwas eingehender mit diesem Thema.

Die CERT Secure Coding Standards enthalten den Eintrag OBJ38-J. Unveränderliche Klassen müssen eine Erweiterung verbieten, die kurz und bündig eine Hauptgrundlage für die Angabe einer unveränderlichen Java-Klasse als endgültig beschreibt:

Indem wir unveränderliche Klassen für endgültig erklären, garantieren wir, dass böswillige Unterklassen, die den Status des Objekts ändern können und gegen Annahmen verstoßen, die Kunden häufig in Bezug auf Unveränderlichkeit treffen, kein Problem darstellen.

In Immutable Objects in Java enthalten die vier Autoren (Haack, Poll, Schafer und Schubert) einen Abschnitt zum Thema "Durchsetzung der Unveränderlichkeit". In diesem Abschnitt behaupten sie, dass Klassen, die unveränderliche Klassen erweitern, auch selbst unveränderlich sein müssen. Die Autoren schlagen vor, dass das endgültige Festlegen der unveränderlichen Klasse eine Möglichkeit wäre, mit einer Situation umzugehen, in der "bösartiger Code versuchen könnte, eine unveränderliche Klasse zu unterordnen" und "Maskerade" als übergeordneten Typ.

In veränderlichen und unveränderlichen Objekten gibt David O'Meara an, dass es zwei Ansätze gibt, um sicherzustellen, dass Methoden in einer Klasse, die unveränderlich sein soll, nicht überschrieben werden. Er unterscheidet zwischen starker Unveränderlichkeit (Klasse wird endgültig gemacht) und schwacher Unveränderlichkeit (Methoden werden einzeln und explizit als endgültig deklariert, anstatt Klasse endgültig zu erklären). In O'Mearas Worten: "Der bevorzugte Weg ist, die Klasse endgültig zu machen" (starke Unveränderlichkeit).

Angesichts der obigen Hintergrunddetails ist es an der Zeit, mit einigen Codebeispielen fortzufahren.

Die folgende FractionKlasse weist einige Merkmale der Unveränderlichkeit auf (z. B. die Felder finalund private):

Fraction.java

package dustin.examples; import java.math.BigDecimal; import java.math.RoundingMode; /** * Example of almost immutable class. */ public class Fraction { /** Fraction's numerator. */ private final long numerator; /** Fraction's denominator. */ private final long denominator; /** Scale used in BigDecimal division. */ private final int scale; /** * Parameterized constructor accepting numerator and denominator for the * fraction represented by me. * * @param newNumerator Numerator of fraction. * @param newDenominator Denominator of fraction. */ public Fraction(final long newNumerator, final long newDenominator) { this.numerator = newNumerator; this.denominator = newDenominator; this.scale = 25; } /** * Parameterized constructor accepting numerator and denominator for the * fraction represented by me along with a scale for my decimal representation. * * @param newNumerator Numerator of fraction. * @param newDenominator Denominator of fraction. * @param newScale Scale of my decimal representation. */ public Fraction(final long newNumerator, final long newDenominator, final int newScale) { this.numerator = newNumerator; this.denominator = newDenominator; this.scale = newScale; } /** * Provide this fraction's numerator. * * @return Numerator of this fraction. */ public long getNumerator() { return this.numerator; } /** * Provide this fraction's denominator. * * @param Denominator of this fraction. */ public long getDenominator() { return this.denominator; } /** * Provide double decimal representation of this fraction. * * @return Decimal (double) representation of this fraction. */ public double getDecimalRepresentation() { return (double) numerator / denominator; } /** * Provide the BigDecimal representation of this fraction. * * @return BigDecimal representation of this fraction. */ public BigDecimal getBigDecimalRepresentation() { final BigDecimal bigNumerator = new BigDecimal(this.numerator); final BigDecimal bigDenominator = new BigDecimal(this.denominator); return bigNumerator.divide(bigDenominator, this.scale, RoundingMode.HALF_UP); } /** * Provide String representation of this fraction. * * @return String representation of this fraction. */ public String getStringRepresentation() { return String.valueOf(this.numerator) + "/" + String.valueOf(this.denominator); } /** * Provide String representation of this fraction. * * @return String representation of this fraction. */ @Override public String toString() { return getStringRepresentation(); } /** * Main function testing this class.' * * @param arguments Command-line arguments; none expected. */ public static void main(final String[] arguments) { final Fraction fractionOne = new Fraction(2,3); System.out.println("2 divided by 3 is " + fractionOne.getDecimalRepresentation()); System.out.println("2 divided by 3 is " + fractionOne.getBigDecimalRepresentation()); } } 

Im obigen Beispiel werden weder die Klasse noch ihre Methoden als deklariert final. Dies ermöglicht das Schreiben einer abtrünnigen Unterklasse, wie in der nächsten Klassenliste für gezeigt RenegadeFraction.

RenegadeFraction.java

package dustin.examples; import java.math.BigDecimal; /** * Class extending the 'immutable' Fraction class. */ public class RenegadeFraction extends Fraction { /** * Parameterized constructor accepting numerator and denominator for the * fraction represented by me. * * @param newNumerator Numerator of fraction. * @param newDenominator Denominator of fraction. */ public RenegadeFraction(final int newNumerator, final int newDenominator) { super(newDenominator, newNumerator); } /** * Provide double decimal representation of this fraction. * * @return Decimal (double) representation of this fraction. */ public double getDecimalRepresentation() { return 6.0; } /** * Provide the BigDecimal representation of this fraction. * * @return BigDecimal representation of this fraction. */ public BigDecimal getBigDecimalRepresentation() { return new BigDecimal(5.0); } /** * Provide String representation of me. * * @return My String representation. */ @Override public String toString() { return "Fraction with numerator " + getNumerator() + " and denominator " + getDenominator(); } } 

Da die übergeordnete FractionKlasse nicht finalund ihre Methode nicht endgültig ist, kann die RenegadeFractionKlasse ihre Methoden überschreiben und unsinnige Ergebnisse zurückgeben. Beachten Sie, dass finalder Konstruktor von auch dann, wenn wir die Hauptmethoden erstellt haben , die RenegadeFractionan seine übergeordnete Klasse übergebenen Konstruktorargumente absichtlich oder versehentlich austauschen kann.

Dieses Beispiel zeigt auch ein schlechtes Verhalten der String () - Implementierung in der Unterklasse. Wenn wir die FractionKlasse erweitern und die Methoden markieren, mit denen wir nicht überschreiben möchten final, kann es zu verwirrenden Problemen bei "anderen" Methoden wie kommen toString(). Wollen wir toString()endgültig machen oder der untergeordneten Klasse erlauben, ihren tatsächlichen Inhalt (und den der Eltern) möglicherweise falsch darzustellen?

Die nächste Codeliste zeigt, wie eine Kinderklasse ihre Eltern gegenüber Kunden falsch darstellen kann. Der Hauptcode hier glaubt, dass es sich um FractionKlasse handelt, und kann sogar einige darauf basierende Annahmen treffen. Die tatsächliche Instanz, die an den Client übergeben wird, ist das RenegadeFractionErgebnis eines vorsätzlichen Missbrauchs oder einer unachtsamen Entwicklung.

DemonstrationMain.java

package dustin.examples; import java.math.BigDecimal; import static java.lang.System.out; /** * Demonstrate how allowing an immutable class to be extended can reduce * immutability, at least from the perspective of its behavior. */ public class DemonstrationMain { /** * Enumeration representing type of fraction to help in readability of output. * * This differentiates between the original Fraction and the extending class * RenegadeFraction. */ public enum FractionType { FRACTION("Fraction"), RENEGADE_FRACTION("Renegade"); private String stringRepresentation; FractionType(final String newStringRepresentation) { this.stringRepresentation = newStringRepresentation; } public String getStringRepresentation() { return this.stringRepresentation; } } /** * Accepts immutable Fraction object and prints its String value. * * @param fraction Fraction whose String value will be printed. * @param type Type of fraction (for ease in reading output). */ public static void printFractionToString(final Fraction fraction, final FractionType type) { out.println("Fraction [" + type.getStringRepresentation() + "] is " + fraction); } /** * Accepts immutable Fraction object and prints its String value. * * @param fraction Fraction whose String value will be printed. * @param type Type of fraction (for ease in reading output). */ public static void printFractionStringRepresentation( final Fraction fraction, final FractionType type) { out.println( "Fraction [" + type.getStringRepresentation() + "] is " + fraction.getStringRepresentation()); } /** * Accepts immutable Fraction object and prints its decimal representation. * * @param fraction Fraction whose String value will be printed. * @param type Type of fraction (for ease in reading output). */ public static void printFractionDecimalValue(final Fraction fraction, final FractionType type) { out.println( "Fraction [" + type.getStringRepresentation() + "] decimal: " + fraction.getDecimalRepresentation()); } /** * Accepts immutable Fraction object and prints its BigDecimal representation. * * @param fraction Fraction whose String value will be printed. * @param type Type of fraction (for ease in reading output). */ public static void printFractionBigDecimalValue(final Fraction fraction, final FractionType type) { out.println( "Fraction [" + type.getStringRepresentation() + "] BigDecimal: " + fraction.getBigDecimalRepresentation()); } /** * Print quotient resulting from division of provided dividend by provided * divisor. * * @param dividend Dividend in division. * @param divisor Divisor in division. */ public static void printExternalDivisionResults( final BigDecimal dividend, final BigDecimal divisor) { out.println( "Division of dividend " + dividend + " by divisor " + divisor + " leads to quotient of " + dividend.divide(divisor)); } /** * Main function for executing immutable object or child of immutable object. * * @param arguments Command-line arguments; none expected; */ public static void main(final String[] arguments) { final Fraction fraction = new Fraction(2,3); final RenegadeFraction renegadeFraction = new RenegadeFraction(2,3); printFractionToString(fraction, FractionType.FRACTION); printFractionToString(renegadeFraction, FractionType.RENEGADE_FRACTION); printFractionStringRepresentation(fraction, FractionType.FRACTION); printFractionStringRepresentation(renegadeFraction, FractionType.RENEGADE_FRACTION); printFractionDecimalValue(fraction, FractionType.FRACTION); printFractionDecimalValue(renegadeFraction, FractionType.RENEGADE_FRACTION); printFractionBigDecimalValue(fraction, FractionType.FRACTION); printFractionBigDecimalValue(fraction, FractionType.RENEGADE_FRACTION); printExternalDivisionResults( new RenegadeBigDecimal(fraction.getNumerator()), new RenegadeBigDecimal(fraction.getDenominator())); printExternalDivisionResults( new RenegadeBigDecimal(renegadeFraction.getNumerator()), new RenegadeBigDecimal(renegadeFraction.getDenominator())); } } 

Wenn die obige Demonstrationsklasse ausgeführt wird, wird ihre Ausgabe wie folgt angezeigt: