7.1 组合语法 7.2 继承语法 7.3 代理 7.4 重载和覆盖 7.5 初始化以及类的加载
目录
7.1 组合语法
组合就是把对象放在新类中,做新类的成员,比如
class A { } class B { private String value1, value2, value3; private A a = new A(); private int integer; private double d; }
如上所示,类A对象作为类B的一个成为,此外,类B还具有类String3个对象,又有基本数据类型integer和d。
如果想要初始化类成为,可以在代码中的4种位置进行
1.在定义对象的地方;
2.在类的构造器中;
3.使用实例初始化,也就是初始化块
4.正要使用这个对象之前,这种方式也称作惰性初始化,可以减少额外的负担。
class A{ } class B { // 1.在定义对象的地方 private String name = new String("小明"); private String sex;//未初始化 private int id;// 未初始化 private A a;// 未初始化 {//4.使用实例初始化 id = 13; } public B() {//2.在构造器中初始化 a = new A(); } public void f() { //4.正要使用这个对象时初始化 if (sex == null) sex = "boy"; System.out.println("I am a " + sex); } }
7.2 继承语法
当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则总是在隐式地从Java的标准根类Object进行继承。
class A { private int a; private double b; private String name; public void f1() {} public void f2() {} } class B extends A { private int c; public void f3() {} }
如上代码所示,使用关键字extends,类B继承了类A,获得了父类所有的属性和方法,包括父类的private属性
class Person { public String toString() { return "I am a person"; } } class Father extends Person { private String name = "小明"; public Father(String name) { this.name = name; } public String toString() { return "I am a father"; } public String getName() { return name; } } class Son extends Father { public Son(String name) { super(name); } public String toString() { return "I am a son"; } } public class PersonTest { public static void main(String[] args) { Father father = new Father("小天"); Son son = new Son("小强"); father.say("My name is" + father.getName()); son.say("I am " + son.getName()); } }
如上代码所示,类Father定义了private修饰的属性name,被类Son继承并在构造器中初始化了;同时类Father定义了访问器getName(),因为是用public修饰的,所以类Son的对象可以直接使用;我们还可以注意到,类Person定义了公共方法toString(),实际上根类Object也定义了toString(),但类Person的toString()是重载了,因为方法签名不一样,类Father也定义了相同方法签名的toString(),这是覆盖类Person的toString(),同理类Son的toString()覆盖了类Father的toString()。
注意到类Son的构造器这一句 super(name); 如果注释掉这一句,或者改为super();编译器只能报错,这是因为继承机制要求要初始化父类:
实际上,继承并不只是复制父类的接口,当创建了一个子类的对象时,该对象包含了一个父类的子对象,这个子对象与你用父类直接创建对象是一样,只不过后者来自于外部,而父类的子对象被包装在子类对象的内部。对父类子对象的正确初始化也是至关重要的,而且也仅仅有一种方法,那就是早构造器中调用父类的构造器来执行初始化
class Art { Art() {System.out.print("Art 构造器");} } class Drawing extends Art { Drawing() {System.out.print("Drawing 构造器");} } class Cartoon extends Drawing { Cartoon() {System.out.print("Cartoon 构造器");} }
如上代码所示,当初始化Cartoon时,发现有父类Drawing,于是编译器会默认在构造器的第一句添加一句super();再先初始化父类Drawing,又发现有父类Art,于是会默认在构造器的第一句添加一句super();再初始化父类Art,对于根类Object就不说了,就这样输出结果为
Art 构造器Drawing 构造器Cartoon 构造器
那构造器的第一行的super(初始化父类的super本来就必须是第一行)能否带有参数呢?当然可以,像上面的例子,类Son的构造器中super(name)就是带有参数的,但若去掉参数name,或者干脆不写,编译器就抱怨无法初始化了,因为父类Father不存在无参构造器。如果遇到下面这样的例子呢
class A { A() { System.out.println("A无参"); } } class B extends A { B() { System.out.println("B无参"); } B(int a) { this(); System.out.println("B有参"); } } class C extends B { C() { super(1); System.out.println("C无参"); } } public class Inheritance { public static void main(String[] args) { new C(); } }
因为这个构造方法链是从初始化对象开始,一直到根类Object结束。所以,当执行this();程序跳转到B()这个构造器,但是此时整个构造方法链条早已经完成了,自然不会super到父类A了,因此程序输出为
A无参 B无参 B有参 C无参
7.3 代理
类之间的关系有组合,继承,还有第三种关系成为代理,Java没有提供对它的直接支持,它是组合与继承的中庸之道。
因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们会在新类中暴露了此成员对象的所有方法(就像继承),代理解决了此难题
class A { f1(int i) {} f2(int i) {} } class B { A a = new A(); f1(int i) { a.f1(i); } f2(int i) { a.f2(i); } }
如上代码所示,在设计时,我们希望使用B的f1和f2方法,而不是直接使用A的f1和f2方法,打个比方,我们希望让一辆汽车向前开,而不是让汽车的控制引擎向前开
7.4 重载和覆盖
重载就是方法名相同,而参数列表不同,允许返回值类型不同,其中参数列表不同在于参数的个数和类型不同;
覆盖就是子类重写了父类的方法,子类的某个方法的方法名,返回值类型和参数列表必须和父类的一个方法一模一样
1 class A { 2 int method(int i) { 3 return i; 4 } 5 } 6 7 public class B extends A { 8 public int method(int i) { 9 return ++i; 10 } 11 12 double method(double d) { 13 return d; 14 } 15 16 double method(int a, double b) { 17 return a + b; 18 } 19 20 public static void main(String[] args) { 21 B c = new B(); 22 System.out.println(c.method(1)); 23 System.out.println(c.method(5.6));24 System.out.println(c.method(5, 5.5)); 25 } 26 }
需要注意的是,如果第8行代码的修饰符改成private,编译器就抱怨修饰符的可见性比父类的method低,因此覆盖时必须保证修饰符的可见性不低于父类的方法;第12到14行是重载了父类的method,其中返回值类型可以不同,第16到18行也是重载了method,其中参数列表的个数不同
7.5 初始化以及类的加载
每个类的编译代码都存在于它自己的独立的文件中,尽管一个java文件可以有多个类。该文件只在需要使用程序代码时才会被加载。一般来说,类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对象之时,但是当访问static 域或static 方法时,也会发生加载。
初次使用之处也是static初始化发生之处。所有的static对象和static代码段都会在加载时依程序中的顺序而依次初始化。当然,定义为static的东西只会被初始化一次。
下面是Java编程思想的一个例子
1 import static java.lang.System.*; 2 3 class Insect { 4 private int i = 9; 5 protected int j; 6 Insect() { 7 out.println("i = " + i + ", j = " + j); 8 j = 39; 9 } 10 11 private static int x1 = 12 printInit("static Insect.x1 initialized"); 13 14 static int printInit(String s) { 15 out.println(s); 16 return 47; 17 } 18 } 19 20 public class Beetle extends Insect { 21 private int k = printInit("Beetle.k initialized"); 22 public Beetle() { 23 out.println("k = " + k); 24 out.println("j = " + j); 25 } 26 27 private static int x2 = 28 printInit("static Beetle.x2 initialized"); 29 30 public static void main(String[] args) { 31 out.println("Beetle constructor"); 32 Beetle b = new Beetle(); 33 } 34 }
在Beetle上运行Java时,所发生的第一件事就是试图访问Beetle.main()(一个static方法),于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class的文件中)。在对它进行加载的过程中,编译器注意到它有一个父类(由关键字extends得知),于是它继续加载,不管你是否打算创建一个该父类的对象,这都要发生。
如果该父类还有其自身的父类,那么第二个父类就会被加载,如此类推。接下来,根父类中的static初始化(在此例中为Insect)即会被执行,然后是下一个子类,以此类推。
至此为止,必要的类都已被加载完毕,对象就可以被创建了。首先,对象中所有的基本类型都会被设为默认值,对象引用被设为null。然后,父类的构造器会被调用。在本例中,它是被自动调用的。但也可以用super来指定对父类构造器的调用(正如在Beetle()的构造器中的第一步操作)。父类构造器和子类的构造器一样,以相同的顺序来经历相同的过程。在父类构造器完成之后,实例变量按其次序被初始化。最后构造器的其余部分被执行。
正如这个例子,在试图访问Beetle.main()时,会先尝试加载Beetle类,但又发现Beetle的父类Insect,于是先加载Insect,Insect的第一个static语句是第11行,而第11行又调用第14行的printInit(String s),打印s并返回47,因此x1=47;然后再加载Beetle,第一个static语句是第27行,而第27行又调用父类的printInit(String s),打印s并返回47,因此x2=47,至此,所有的类已加载完毕;回到Beetle.main(),执行第31行的语句,然后执行第32行,创建Beetle对象,于是尝试执行第22行的构造器,但发现有父类Insect,因而先初始化父类,程序跳转到第6行,调用父类的构造器,当父类初始化完毕,再初始化子类,以此类推,才成功创建Beetle对象。