1Z0-809复习 第1章 Advanced Class Design

Advanced Class Design

Reviewing OCA Concepts

Access Modifiers

Overloading and Overriding

Abstract Classes

Static and Final

Imports

Using instanceof

A instanceof B,如果A是B,或B的子类,则是true,如果不是则是false。
如果A是null,则永远是false,如果B是Object,除非A是null,否则永远是true。

Understanding Virtual Method Invocation

virual method :首先是非static得方法。 例如子类override了父类的一个方法,那么在实际调用该方法的时候。 java会寻找使用合适的方法,而不是根据编译的类里面的那个方法。

这个在以前也OCA里面已经有这个概念,在OCP中只是明确了叫virtual method的方法。

如果没有A跟比没有任何继承关系,那么会出CompileError。

如果B是interface,那么会在运行期再进行检查。

public interface Mother {}
class Hippo extends HeavyAnimal { }

HeavyAnimal hippo = new Hippo();
boolean b6 = hippo instanceof Mother;

上面这段source不会出错。 如果后续有下面一个定义。

class MotherHippo extends Hippo implements Mother { }
abstract class Animal {
    String name = "???";
    public void printName() {
        System.out.println(name);
    }
}
class Lion extends Animal {
    String name = "Leo";
}
public class PlayWithAnimal {
    public static void main(String... args) {
        Animal animal = new Lion();
        animal.printName();
    }
}

成员变量跟成员方法不一样,不存在override,
其只存在hidden,因此这个输出结果是???而不是Leo。 这也是多态的一个特点。

abstract class Animal {
    public void careFor() {
        play();
    }
    public void play() {
        System.out.println("pet animal");
    }
}
class Lion extends Animal {
    @Override
    public void play() {
        System.out.println("toss in meat");
    }
}
public class PlayWithAnimal {
    public static void main(String... args) {
        Animal animal = new Lion();
        animal.careFor();
    }
}

生成了一个lion的对象,参照类型设置为父类Animal。
因为careFor只在animal里面有,所以调用的是Animal的careFor。
Animal的careFor调用了play,但这个时候java会看play是否有重载。
发现lion中有play的override方法,因此调用Lion里面的play。
即使参照对象是父类Animal,调用的还是子类Lion的play,这个就是多态。

Annotating Overridden Methods

override方法可以用注释@override显示声明。
annotation是一种元数据,可以在编译期或实行期使用。

当出现@override,说明有以下3种情况之一

  • 实现了接口的某个方法。
  • override了父类的某个方法。
  • override了Object的方法,例如toString,equals,hashCode。

Coding equals, hashCode, and toString

java中所有类都是直接或间接继承了Object,因此也继承了Object类的所有方法。

toString

java中的print方法,会自动调用打印对象的toString()方法。

对象如果实装了toString()方法,使得打印的对象内容更清楚明了。
譬如ArrayList,[1,2,3]
如果对象使用Object的toString()方法,例如array对象
[Ljava.lang.String;@65cc892e]

一般的做法是打印出对象的所有成员变量,但一个一个写的话非常麻烦。
Apache社区提供了上述opensource,利用其提供的方法,
可以非常方便的打印出对象中所有成员变量。

//定义build.gradle
compile("org.apache.commons:commons-lang3:3.8")
//方法重载
public String toString() {
    return ToStringBuilder.reflectionToString(this);
}

这其中利用到了反射,在实行期取得类的信息。
所有source:

import org.apache.commons.lang3.builder.ToStringBuilder;

public class Hippo {
    private String name;
    private double weight;
    public Hippo(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
    public static void main(String[] args) {
        Hippo h1 = new Hippo("Harry", 3100);
        System.out.println(h1); // Harry
    }
}

执行结果:com.apibot.Hippo@78308db1[name=Harry,weight=3100.0]

//简略版
public String toString() {
    return ToStringBuilder.reflectionToString(this,ToStringStyle.SHORT_PREFIX_STYLE);
}

执行结果:Hippo[name=Harry,weight=3100.0]

equals

如果类重载了equals方法,那么利用重载的方法进行比较。

String s1 = new String("lion");
String s2 = new String("lion");
System.out.println(s1.equals(s2)); // true
StringBuilder sb1 = new StringBuilder("lion");
StringBuilder sb2 = new StringBuilder("lion");
System.out.println(sb1.equals(sb2)); // false

这个应该是在StringBuilder类中equals没有重载,
因此用的是Object的equals方法来比较。
而String在重载的equals方法中,比较的是每个字符串的每个字符,所以是相等的。
- reflexive :反身性的,x.equals(x) 肯定是true
- symmetric: :对称的,如果x.equals(y)是true,那么y.equals(x)肯定也是true
- transitive: :传递性的,如果x.equals(y)是true,y.equals(z),那么 x.equals(z)也应该是相等的。
- consistent: : - x.equals(null) 永远都是false。而不是出NullPointerException

public boolean equals(Lion obj) {
    if (obj == null) return false;
    return this.idNumber == obj.idNumber;
}

传进来的参数如果不是Object类型,那么这个就不是Override,而是Overload。
虽然没有任何问题,但还是要注意

public boolean equals(Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj);
}

重载之后比较的就是对象的所有成员变量。

public boolean equals(Object obj) {
    if ( !(obj instanceof LionEqualsBuilder)) return false;
    Lion other = (Lion) obj;
    return new EqualsBuilder().appendSuper(super.equals(obj))
                              .append(idNumber, other.idNumber)
                              .append(name, other.name)
                              .isEquals();
}

重载之后,比较的是对象中的idNumber和name。 虽然不是很简洁,还是得手动处理null和instanceof,但毕竟比所有都全手写要方便。

hashCode

当把object作为key存到map里去的话,会需要用到hashcode

public class Card {
    private String rank;
    private String suit;
    public Card(String r, String s) {
        if (r == null || s == null) {
            throw new IllegalArgumentException();
        }
        rank = r;
        suit = s;
    }
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Card)) {
            return false;
        }
        Card c = (Card) obj;
        return rank.equals(c.rank) && suit.equals(c.suit);
    }
    @Override
    public int hashCode() {
        return rank.hashCode();
    }
}

如果是primitive数字,可以直接用,或者取更小的int作为hashcode。
另外,一般在hashcode中不包含boolean和char类型的变量。

  • 同样的program,hashcode()的结果不会变。
    意味着不可以包含会变的变量。
    前面的Hippo类例子,只包括名字,因为体重会变。

  • 如果equals()的结果是true,那么hashCode()会返回相同的结果。
    这个意味着hashCode使用equals比较的成员变量的子集。
    例如上面Card的例子,有两个变量用于equals,但只有其中的一个用于hashCode。

  • 另一个角度讲,如果equals()的结果是false,hashCode()并不需要返回相同的结果。

//没问题,hashCode和equals用相同的变量
public int hashCode() { return idNumber; }
//没问题,但没有意义,永远返回相同的6
public int hashCode() { return 6; }
//方法名里的c不是C,因此是一个全新的方法。如果是C也有问题,override的时候返回值类型只能是子类。
public long hashcode() { return idNumber; }
//这个有问题,因为用到的参数比equals多,idNumber以外用了age
public int hashCode() { return idNumber * 7 + age; }

Working with Enums

public enum Season {
    WINTER, SPRING, SUMMER, FALL //没有分号
}

Enum值最后是没有分号的。
Enum和常数的区别在于,其是Type Safe,也就是在编译之前就知道这些成员变量的type。
enum包含的是static变量,以及helper method。

Season s = Season.SUMMER;

关于这个用法稍微有点违和感,貌似把类的成员变量赋予了类。

System.out.println(s == Season.SUMMER); // true

看上去像是两个字符串用等号来比较,有违和感。
可以想象成这是两个一开始就定义好的static final的变量。
这样用等号就没有违和感了。
enum的toString()起作用时,取得是enum的name

for(Season season: Season.values()) {
    System.out.println(season.name() + " " + season.ordinal());
}
System.out.println(Season.SPRING.name() + " " + Season.SPRING.ordinal());

这个看上去也有违和感。是因为Season的静态方法values()会取得其中的所有元素。
循环这个array,用name()方法取得每个元素的名字。用ordinal()方法,取得每个元素的值。

可以看出来每一个元素都有一个name一个顺序int值

Season s1 = Season.valueOf("SUMMER"); // SUMMER
Season s2 = Season.valueOf("summer"); // java.lang.IllegalArgumentException

可以用完全一模一样的字符串创建一个enum

Season.SPRING.values(); // Season的所有元素,少用
Season.values();        // Season的所有元素,相同

Using Enums in Switch Statements

Season summer = Season.SUMMER;
switch (summer) {
//这个用法是错的,因为:
//Season.WINTER是一个enum,并不是enum的int
//WINTER可以称为一个enum的value,是case可以接受的类型。
//Season可以称作enum的type。
case Season.WINTER:
    System.out.println("Get out the sled!");
    break;
case SUMMER:
    System.out.println("Time for the pool!");
    break;
default:
    System.out.println("Is it summer yet?");
}

最终针对Season.SUMMER了解几个概念:

Adding Constructors, Fields, and Methods

public enum Season {
    WINTER("Low"), SPRING("Medium"), SUMMER("High"), FALL("Medium");
    //成员变量
    private String expectedVisitors;
    //构造函数
    private Season(String expectedVisitors) {
        this.expectedVisitors = expectedVisitors;
    }
    //成员方法
    public void printExpectedVisitors() {
        System.out.println(expectedVisitors);
    }
}
  • 如果enum类里面只有value的一行,那么最后一个分号是可以省略的。
    像上面的情况,最后一个分号就不可以省略。
  • 构造方法的修饰词必须是private,因为只可以在enum之内被调用。public是无法通过编译的。
  • 构造函数可以看出来,是将方法的形参赋给成员变量。 再看第一行的定义就明白了,Low,Medium,High就是形参的具体值。
Season.SUMMER.printExpectedVisitors();

执行结果是High,因为SUMMER("High")调用构造函数把High赋给了SUMMER 另一enum的用法:

public enum Season {
    WINTER {
        @Override
        public void printHours() {
            System.out.println("9am-3pm");
        }
    },
    SPRING {
        @Override
        public void printHours() {
            System.out.println("9am-5pm");
        }
    },
    SUMMER {
        @Override
        public void printHours() {
            System.out.println("9am-7pm");
        }
    },
    FALL {
        @Override
        public void printHours() {
            System.out.println("9am-5pm");
        }
    };
    public abstract void printHours();
}
Season summer = Season.SUMMER;
summer.printHours();

Creating Nested Classes

嵌套类是定义在另一个类里面的类。
如果是非static,则叫做内部类inner class。

  • 成员内部类和成员变量同等级定义,非static。
    通常是指不显示定义类型的内部类。
  • 本地内部类,定义在方法体内。
  • 匿名内部类是一个没有名字的本地内部类的。
  • 静态嵌套类,是跟static变量同级别的静态类。

用嵌套类的好处之一:就是可以封装helper class到容器类中

Member Inner Classes

member level(和成员方法,成员变量和构造方法一级)定义的类。
- 可以定义,public,private,protected,default access
- 可以继承任何其他类或接口 - 可以是抽象类和final - 不可以定义静态成员和方法。 - 可以访问外部类的成员变量,包括private成员变量。 最后两条非常重要。

public class Outer {
    private String greeting = "Hi";
    protected class Inner {
        public int repeat = 3;
        public void go() {
            for (int i = 0; i < repeat; i++) {
                System.out.println(greeting);
            }
        }
    }
    public void callInner() {
        Inner inner = new Inner();
        inner.go();
    }
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.callInner();
        Outer outer2 = new Outer();
        // 一开始以为应该是 new outer2.Inner();
        // 但我错了,要记住这个格式
        Inner inner2 = outer2.new Inner();
    }
}

编译后的.class文件。一个是Outer.class,一个是Outer$Inner.class InnerClass可以和OuterClass有相同名字的成员变量。

public class A {
    private int x = 10;
    class B {
        private int x = 20;
        class C {
            private int x = 30;
            public void allTheX() {
                System.out.println(x);        // 30
                System.out.println(this.x);   // 30
                System.out.println(B.this.x); // 20
                System.out.println(A.this.x); // 10
            }
        }
    }
    public static void main(String[] args) {
        A a = new A();
        A.B b = a.new B();
        A.B.C c = b.new C();
        c.allTheX();
    }
}

可以从A.B省略成B,因为类B和main方法在同一个member level
但不可以把A.B.C省略成C,因为C在下一层。
另外可以增加一句import A.B.C,这样就可以直接用C了。

private interface
public class CaseOfThePrivateInterface {
    private interface Secret {
        public void shh();
    }
    class DontTell implements Secret {
        @Override
        public void shh() {
        }
    }
}

关于interface的规则没有变: - 成员方法缺省都是public,实装的方法必须是public的。
- interface本身不必须是public,跟内部类相同,inner interface可以是private的。
同时也意味着这个interface只能被outerclass使用。

Local Inner Classes

local inner class 定义直到方法被调用的时候才产生。
然后到方法返回时就失效了。
所有的动作跟成员变量相同。
- 不可以有修饰符。 - 不能声明为static,也不能有static的成员变量和方法。 - 可以访问包围类的所有成员和方法。 - 他们没有权限访问本地变量,除非这些变量是final或者等效final。

public class Outer {
    private int length = 5;
    public void calculate() {
        //这里是final所有没有问题
        //如果是 int width = 20; width = 30; 就会出错。
        final int width = 20;
        class Inner {

            public void multiply() {
                System.out.println(length * width);
            }
        }
        Inner inner = new Inner();
        inner.multiply();
    }
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.calculate();
    }
}

一个是instance变量,一个是final变量,因此在innerclass内可以访问。

effecitvely final

不需要显示定义,实际上只发生一次赋值的变量都是 effecitvely final

Anonymous Inner Classes

就是没有名字的Local Inner Class。

public class AnonInner {
    abstract class SaleTodayOnly {
        abstract int dollarsOff();
    }
    public int admission(int basePrice) {
        SaleTodayOnly sale = new SaleTodayOnly() {
            @Override
            int dollarsOff() {
                return 3;
            }
        };
        return basePrice - sale.dollarsOff();
    }
}

看上去像是new了一个抽象类,抽象类是不能被new的。
但只要有方法体{},就可以(多态?)
不是抽象类,接口也可以。 interface 的方法缺省是public的。
但抽象类的方法,是default的。
无法继承一个class的同时实现一个接口。
除非要继承的是java.lang.Object <-这句话什么意思。

如果真有这个需求,那么最好是定义成local inner class
然后然后就可以继承类和实现接口了。
如果真的需要这么复杂,内部类本身就可能并不是一个合适的选择。

public class AnonInner {
    interface SaleTodayOnly {
        int dollarsOff();
    }
    public int pay() {
        return admission(5, new SaleTodayOnly() {
            @Override
            public int dollarsOff() {
                return 3;
            }
        });
    }
    public int admission(int basePrice, SaleTodayOnly sale) {
        return basePrice - sale.dollarsOff();
    }
}

Static Nested Classes

静态类的用法跟静态方法一样,例如不能直接访问外部类的成员变量。

public class Enclosing {
    static class Nested {
        //静态类里面也可以定义静态方法,但是需要有静态类对象才能访问。
        private int price = 6;
    }
    public static void main(String[] args) {
        //初始化的时候不需要Enclosing的对象就可以初始化,这跟内部成员类有区别。
        Nested nested = new Nested();
        //生成了静态类的对象,然后访问。
        System.out.println(nested.price);
    }
}

import静态类时的两个方法。
普通方法: import Enclosing.Nested
静态导入: import static Enclosing.Nested

不是静态内部类,只能用普通方法import。
嵌套类import的时候,outerclass看上更像是一个namespace。

Summary

Exam Essentials

Review Questions