1. 程式人生 > >Effective java筆記-對於所有物件都通用的方法

Effective java筆記-對於所有物件都通用的方法

對於所有物件都通用的方法

第8條 覆蓋equals時請遵守通用約定

滿足以下任何一個條件,可以不覆蓋equals:
類的每個例項本質上都是唯一的。(如Thread)
不關心是否提供了“邏輯相等”的測試功能(如Random)
超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。(如Set)
類是私有的或包級私有的,可以確定他的equals方法永遠不會被呼叫
當類有自己的邏輯相等的概念且父類沒有實現期望的功能時要覆蓋equals,且覆蓋之後還可以使這個“類的值”可以作為key,一般這種類都是一些“值類”,如String,Integer。不過有一種“值類”不需要覆蓋equals,就是例項受控的類,這種類只會存在一個物件,所以物件相等和值相等就是一回事了。
覆蓋equals方法要遵守一些規範(equals方法實現了等價關係):
自反性,對稱性,傳遞性,一致性(非null x,y只要equals的比較操作在物件中所用的資訊沒有被修改,則多次呼叫x。equals(y)返回值時一致的),x.equals(null)必需返回false(x非null)
對稱性例子:
// Broken - violates symmetry! - Pages 36-37
package org.effectivejava.examples.chapter03.item08;

public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this
.s = s; } // Broken - violates symmetry! @Override public boolean equals(Object o) { if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) o).s); if (o instanceof String) // One-way interoperability! return s.equalsIgnoreCase((String) o); return
false; } // This version is correct. // @Override public boolean equals(Object o) { // return o instanceof CaseInsensitiveString && // ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); // } public static void main(String[] args) { CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); String s = "polish"; System.out.println(cis.equals(s) + " " + s.equals(cis)); } }
傳遞性例子:
// Simple immutable two-dimensional integer point class - Page 37
package org.effectivejava.examples.chapter03.item08;

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }


    // See Item 9
    @Override
    public int hashCode() {
        return 31 * x + y;
    }
}
上面程式碼定義了一個Point類,它們的邏輯相等條件是x,y相等,現在再定義一個ColorPoint,繼承自Point:
public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

// Attempting to add a value component to Point - Pages 37 - 38
package org.effectivejava.examples.chapter03.item08;

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }


    public static void main(String[] args) {
        // First equals function violates symmetry
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2, Color.RED);
        System.out.println(p.equals(cp) + " " + cp.equals(p));

    }
}
你會發現這樣違反了對稱性,改成這樣,又會違反傳遞性
// Broken - violates transitivity!
     @Override public boolean equals(Object o) {
         if (!(o instanceof Point))
         return false;

         If o is a normal Point, do a color-blind comparison
         if (!(o instanceof ColorPoint))
         return o.equals(this);

         o is a ColorPoint; do a full comparison
         return super.equals(o) && ((ColorPoint)o).color == color;
     }

// Second equals function violates transitivity
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
        System.out.printf("%s %s %s%n", p1.equals(p2), p2.equals(p3),
                p1.equals(p3));
你可能這樣實現Point的equals方法:
// Broken - violates Liskov substitution principle - Pages 39-40
    // @Override public boolean equals(Object o) {
    // if (o == null || o.getClass() != getClass())
    // return false;
    // Point p = (Point) o;
    // return p.x == x && p.y == y;
    // }
這樣看起來還可以,但結果是不可接受的,假設我們通過不新增值元件的方式擴充套件了Point
// Trivial subclass of Point - doesn't add a value component - Page 39
package org.effectivejava.examples.chapter03.item08;

import java.util.concurrent.atomic.AtomicInteger;

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public int numberCreated() {
        return counter.get();
    }
}
假設又有一個方法是用來判斷某個Point是否是在單位圓中的整值點:
// Test program that uses CounterPoint as Point
package org.effectivejava.examples.chapter03.item08;

import java.util.HashSet;
import java.util.Set;

public class CounterPointTest {
    // Initialize UnitCircle to contain all Points on the unit circle
    private static final Set<Point> unitCircle;
    static {
        unitCircle = new HashSet<Point>();
        unitCircle.add(new Point(1, 0));
        unitCircle.add(new Point(0, 1));
        unitCircle.add(new Point(-1, 0));
        unitCircle.add(new Point(0, -1));
    }

    public static boolean onUnitCircle(Point p) {
        return unitCircle.contains(p);
    }

    //里氏替換法則認為一個型別的任何重要屬性也將適用於其子型別,因此為該方法定義的方法也應該在子型別上執行地很好,可是:
    public static void main(String[] args) {
        Point p1 = new Point(1, 0);
        Point p2 = new CounterPoint(1, 0);

        // Prints true
        System.out.println(onUnitCircle(p1));

        // Should print true, but doesn't if Point uses getClass-based equals
        System.out.println(onUnitCircle(p2));
    }
}
到這裡就說明了Point不能使用基於getClass地equals方法,那這樣ColorPoint又怎麼辦呢,也就是說如何即擴充套件不可例項化的(?)類,又增加值元件(ColorPoint增加了值元件,CounterPointTest沒有增加),有一個權宜之計:使用組合,以下是最終的完整解決方案:
// Simple immutable two-dimensional integer point class - Page 37
package org.effectivejava.examples.chapter03.item08.composition;

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }

    // See Item 9
    @Override
    public int hashCode() {
        return 31 * x + y;
    }
}

package org.effectivejava.examples.chapter03.item08.composition;

public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

// Adds a value component without violating the equals contract - Page 40
package org.effectivejava.examples.chapter03.item08.composition;

public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        if (color == null)
            throw new NullPointerException();
        point = new Point(x, y);
        this.color = color;
    }

    /**
     * Returns the point-view of this color point.
     */
    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }

    @Override
    public int hashCode() {
        return point.hashCode() * 33 + color.hashCode();
    }
}
一致性:不要使equals方法依賴於不可靠的資源。java.net.URL就依賴於URL中主機IP地址的比較,這違反了一致性,不過因為相容性的需要無法改變這個行為。
覆蓋equals時總是覆蓋hashCode方法;
不要將equals宣告中的Object物件替換為其他型別

第九條 覆蓋equals時總是覆蓋hashCode

覆蓋equals時如果不覆蓋hashCode就會導致該類無法結合所有基於雜湊的集合一起工作。
Object規範:
1.equals基於的比較資訊不變則hashCode也不能變
2.x.equals(y)==true,則x.hashCode()==y.hashCode()

3.x.equals(y)==false,x.hashCode()和y.hashCode()不一定不同,但要知道盡可能使得兩個不相等(equals返回false)的物件的hashCode不同可提高散列表效能

package org.effectivejava.examples.chapter03.item09;

// Shows the need for overriding hashcode when you override equals - Pages 45-46

import java.util.HashMap;
import java.util.Map;

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
            throw new IllegalArgumentException(name + ": " + arg);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    // Broken - no hashCode method!

    // A decent hashCode method - Page 48
    // @Override public int hashCode() {
    // int result = 17;
    // result = 31 * result + areaCode;
    // result = 31 * result + prefix;
    // result = 31 * result + lineNumber;
    // return result;
    // }

    // Lazily initialized, cached hashCode - Page 49
    // private volatile int hashCode; // (See Item 71)
    //
    // @Override public int hashCode() {
    // int result = hashCode;
    // if (result == 0) {
    // result = 17;
    // result = 31 * result + areaCode;
    // result = 31 * result + prefix;
    // result = 31 * result + lineNumber;
    // hashCode = result;
    // }
    // return result;
    // }

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
    }
}
在雜湊碼的計算過程中,可以把冗餘域排除在外(冗餘域就是可以通過其他域的值計算出來的),必需排除equals比較計算中沒有用到的域,否則會違反上面第二條。

第十條 始終要覆蓋toString

比較簡單,直接給個例子好了:
// Adding a toString method to PhoneNumber - page 52
package org.effectivejava.examples.chapter03.item10;

import java.util.HashMap;
import java.util.Map;

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
            throw new IllegalArgumentException(name + ": " + arg);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    /**
     * Returns the string representation of this phone number. The string
     * consists of fourteen characters whose format is "(XXX) YYY-ZZZZ", where
     * XXX is the area code, YYY is the prefix, and ZZZZ is the line number.
     * (Each of the capital letters represents a single decimal digit.)
     * 
     * If any of the three parts of this phone number is too small to fill up
     * its field, the field is padded with leading zeros. For example, if the
     * value of the line number is 123, the last four characters of the string
     * representation will be "0123".
     * 
     * Note that there is a single space separating the closing parenthesis
     * after the area code from the first digit of the prefix.
     */
    @Override
    public String toString() {
        return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
    }

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        System.out.println(m);
    }
}

第十一條 謹慎地覆蓋clone

Cloneable介面的目的是作為物件的一個mixin介面(見18條),表明這樣的物件允許克隆。
既然Cloneable並沒有包含任何方法,那他到底有什麼用呢:他會改變Object中受保護的clone方法,如果一個類實現了Cloneable,Object的clone方法就返回該物件的逐域拷貝,否則就會丟擲CloneNotSupportedException異常,這是介面的一種極端非典型用法,不值得效仿。
還有Object的clone方法是protected的,那我們要怎麼用,只要子類繼承,並將子類中的clone改為public就可。(我們一般會在子類的clone中呼叫super。clone,後面會說)
Colne方法的通用約定是非常弱的:
    一般:x.clone()!=x為true;x.clone().getClass()==x.getClass() 為true;x。clone().equals(x)為true;這些不是絕對的要求。
        還有要求這個過程中沒有呼叫構造器。
這個約定存在幾個問題:
    1.不呼叫構造器太強硬了
    2.x.clone().getClass()通常應該等於x.getClass()又太軟弱了(??)
在實踐中,程式設計師會假設:如果它們擴充套件了一個類,並且從子類中呼叫了super.clone(),返回的物件將是該子類的例項。超類能夠提供這種功能的唯一途徑是,返回一個通過呼叫super.clone而得到的物件。如果clone方法返回一個由構造器建立的物件,他就得到有錯誤的類。如果所有的超類都遵守這條規則,那麼呼叫super.clone最終會呼叫Object的clone方法,從而創建出正確類的例項。(書上這裡沒怎麼看懂,但可以知道的是,要遵守規則:返回的物件是通過super.clone建立的,而不是自己呼叫構造器建立的)
例子:
// Adding a clone method to PhoneNumber - page 55
package org.effectivejava.examples.chapter03.item11;

import java.util.HashMap;
import java.util.Map;

public final class PhoneNumber implements Cloneable {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
            throw new IllegalArgumentException(name + ": " + arg);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    /**
     * Returns the string representation of this phone number. The string
     * consists of fourteen characters whose format is "(XXX) YYY-ZZZZ", where
     * XXX is the area code, YYY is the prefix, and ZZZZ is the line number.
     * (Each of the capital letters represents a single decimal digit.)
     * 
     * If any of the three parts of this phone number is too small to fill up
     * its field, the field is padded with leading zeros. For example, if the
     * value of the line number is 123, the last four characters of the string
     * representation will be "0123".
     * 
     * Note that there is a single space separating the closing parenthesis
     * after the area code from the first digit of the prefix.
     */
    @Override
    public String toString() {
        return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
    }

    @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // Can't happen
        }
    }

    public static void main(String[] args) {
        PhoneNumber pn = new PhoneNumber(707, 867, 5309);
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        m.put(pn, "Jenny");
        System.out.println(m.get(pn.clone()));
    }
}
物件中有可變的物件時clone,例子:
// A cloneable version of Stack - Pages 56-57
package org.effectivejava.examples.chapter03.item11;

import java.util.Arrays;

public class Stack implements Cloneable {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    // Ensure space for at least one more element.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

    // To see that clone works, call with several command line arguments
    public static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : args)
            stack.push(arg);
        Stack copy = stack.clone();
        while (!stack.isEmpty())
            System.out.print(stack.pop() + " ");
        System.out.println();
        while (!copy.isEmpty())
            System.out.print(copy.pop() + " ");
    }
}
實際上,clone方法就是另一個構造器;你必須確保它不會傷害到原始的物件,並確保正確地建立被克隆物件中的約束條件(invariant)
如果elements域是final的,上述方案就不能正常工作,因為clone方法是被禁止給elements域賦新值。這是個根本的問題:clone架構與引用可變物件的final域的正常用法是不相相容的,除非在原始物件和克隆物件之間安全地共享此可變物件。
有時候這樣還要這樣:
public class HashTable implements Cloneable{
    private Entry[] buckets = ...;
    private static class Entry{
        final Object key;
        Object value;
        Entry next;
        Entry(Object key,Object value,Entrt next){
            this.key = key;
            this.value=value;
            this.next = next;
        }
        protected Object deepCopy() {
            return new Entry(hash, key, value,
                                  (next==null ? null : next,deepCopy()));
        }
    }
    public HashTable clone(){
        try{
            HasbTable result = (HasbTable)super.clone();
            result.buckets = new Entry[buckets.length];
            for(int i=0;i<buckets.length;i++)
                if(buckets[i]!=null)
                    result.buckets[i]=buckets[i].deepCopy();
            return result;
        }catch(。。。){
            。。。
        }
    }

}
不過看了這段程式碼,到不知道為什麼上面那個類不用deepCopy呢???
和構造器一樣,clone方法不應該在構造地過程中,呼叫新物件中任何非final的方法。如果clone呼叫了一個被覆蓋的方法,那麼在該翻噶發所在的子類有機會修正他在克隆物件的狀態前,該方法就會被執行,這樣可能導致克隆物件和原始物件之間的不一致。
Object的clone方法被宣告為可丟擲CloneNotSupportedException,公有的clone應該省略這個宣告(為了好用),如果專門為了繼承而設計的類覆蓋了clone方法,這個clone方法就應該宣告為protected和可丟擲CloneNotSupportedException,並且不實現Cloneable。
實現拷貝還可以用拷貝構造器或拷貝工廠:
拷貝構造器只是一個構造器,它唯一的引數型別是包含該構造器的類,如public Yum(Yum yum);
拷貝工廠是類似於拷貝構造器的靜態工廠,如:public static Yum newInstance(Yum yum);
拷貝構造器和拷貝工廠比clone方法有更多優勢  
而且使用拷貝構造器或者拷貝工廠來代替clone方法時,並沒有放棄介面的功能特性,更進一步,拷貝構造器或者拷貝工廠可以帶一個引數,引數型別是通過該類實現的介面。例如所有通用集合實現都提供了一個拷貝構造器,它的引數型別為Collection或Map。基於介面的拷貝構造器或拷貝工廠(更準確地叫法是轉換構造器和轉換工廠),允許客戶選擇拷貝地實現型別,而不是強迫客戶接受原始地實現型別。例如:假設你有一個HashSet,並且希望把它拷貝成一個TreeSet可以使用轉換構造器:new TreeSet(s).
由於clone方法具有那麼多缺點,有些專家級的程式設計師乾脆從來不去覆蓋clone方法,也從來不去呼叫它,除非拷貝陣列。

第十二條 考慮實現Comparable介面

下面的程式依賴於String實現了Comparable介面,它去掉了命令列引數列表中的重複引數,並按字母順序打印出來:
public class WordList {
    public static void main(String[] args) {
        Set<String> s = new TreeSet<String>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}
java平臺類庫中所有值類(value classes)都實現了Comparable介面
規範:
1.sgn(x.compareTo(y))==-sgn(y.compareTo(x))為true
2.(x.compareTo(y)>0&&y.compareTo(z)>0)=>x.compareTo(z)>0
3.x.compareTo(y)==0=>所有z滿足sgn(x.compareTo(z))==sgn(y.compareTo(z))
  4.強烈建議:(x.compareTo(y)==0)==(x.equals(y)),不過這並非絕對必要,一般來說若違反這條規定,應當說明,推薦這樣的說法:“注意:該類具有內在的排序功能,但是與equals不一致(inconsistent with equals)“
針對equals的的權宜之計也同樣適用於compareTo方法。如果你想為一個實現了Compareable介面的類增加值元件,請不要擴充套件這個類,而是要編寫一個不相關的類,其中包含第一個類的一個例項。然後提供一個檢視(view)方法返回這個例項。這樣既可以讓你自由地在第二個類上實現compareTo方法,同時也允許它的客戶端在必要的時候,把第二個類的例項視同第一個類的例項
關於第四條建議:
    如果一個類的equals和compareTo不一致,如果有一個有序集合包含這個類的元素,那這個集合就無法遵守相關集合介面(Collection,Set,Map)的通用約定,因為對於這些介面的約定是根據equals來定義的,而有序集合使用了compareTo的等同性測試,舉個例子:
    BigDecimal的equals和compareTo不一致,如果你將new BigDecimal("1.0")和new BigDecimal("1.00")到HashSet中,這個HashSet將包含兩個元素,但如果將它們add入TreeSet中,TreeSet將只包含一個元素。
一種compareTo實現方式:
// Making PhoneNumber comparable - Pages 65-66
package org.effectivejava.examples.chapter03.item12;

import java.util.NavigableSet;
import java.util.Random;
import java.util.TreeSet;

public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
            throw new IllegalArgumentException(name + ": " + arg);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    /**
     * Returns the string representation of this phone number. The string
     * consists of fourteen characters whose format is "(XXX) YYY-ZZZZ", where
     * XXX is the area code, YYY is the prefix, and ZZZZ is the line number.
     * (Each of the capital letters represents a single decimal digit.)
     * 
     * If any of the three parts of this phone number is too small to fill up
     * its field, the field is padded with leading zeros. For example, if the
     * value of the line number is 123, the last four characters of the string
     * representation will be "0123".
     * 
     * Note that there is a single space separating the closing parenthesis
     * after the area code from the first digit of the prefix.
     */
    @Override
    public String toString() {
        return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
    }

    @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // Can't happen
        }
    }

    // Works fine, but can be made faster
    // public int compareTo(PhoneNumber pn) {
    // // Compare area codes
    // if (areaCode < pn.areaCode)
    // return -1;
    // if (areaCode > pn.areaCode)
    // return 1;
    //
    // // Area codes are equal, compare prefixes
    // if (prefix < pn.prefix)
    // return -1;
    // if (prefix > pn.prefix)
    // return 1;
    //
    // // Area codes and prefixes are equal, compare line numbers
    // if (lineNumber < pn.lineNumber)
    // return -1;
    // if (lineNumber > pn.lineNumber)
    // return 1;
    //
    // return 0; // All fields are equal
    // }

    public int compareTo(PhoneNumber pn) {
        // Compare area codes
        int areaCodeDiff = areaCode - pn.areaCode;
        if (areaCodeDiff != 0)
            return areaCodeDiff;

        // Area codes are equal, compare prefixes
        int prefixDiff = prefix - pn.prefix;
        if (prefixDiff != 0)
            return prefixDiff;

        // Area codes and prefixes are equal, compare line numbers
        return lineNumber - pn.lineNumber;
    }

    public static void main(String[] args) {
        NavigableSet<PhoneNumber> s = new TreeSet<PhoneNumber>();
        for (int i = 0; i < 10; i++)
            s.add(randomPhoneNumber());
        System.out.println(s);
    }

    private static final Random rnd = new Random();

    private static PhoneNumber randomPhoneNumber() {
        return new PhoneNumber((short) rnd.nextInt(1000),
                (short) rnd.nextInt(1000), (short) rnd.nextInt(10000));
    }
}