Java讲义第五章学习笔记
chapter 5 面向对象(上)
5.1 类和对象
--5.1.1 定义类
[修饰符] class 类名 { 零到多个构造器定义... 零到多个成员变量... 零到多个方法... }
标注:①修饰符可以是 public、final、abstract,或者完全省略这三个标识符。
②static修饰的成员不能访问没有static修饰的成员。
③如果没有为类编写构造器,则使用默认构造器。
④定义成员变量的语法格式如下:[修饰符] 类型 成员变量名 [ = 默认值]
定义方法的语法格式如下:
[修饰符] 方法返回值类型 方法名(形参列表) { //由零条到多条可执行语句组成的方法体 }
注意:其中修饰符可以省略,也可以是public,protected,private,static,final,abstract。其中final和abstact只能出现一个。
命名规则:
类名:一个或多个单词连缀而成,每个单词首字母大写,单词之间不要任何分隔符。
成员变量名和方法名:一个或多个单词连缀而成,第一个单词首字母小写,其余单词首字母大写,单词之间不要任何分隔符。
static 修饰的成员变量和方法称作 :类变量,类方法。不使用static修饰的普通方法,成员变量则属于该类的单个实例,也称作实例变量,实例方法。
定义构造器的语法格式如下:
[修饰符] 构造器名(形参列表) { //由零条或多条可执行语句组成的构造器执行体
}
①其中修饰符可以省略,也可以是public,protected,private其中之一。
②构造器不能定义返回值类型,且构造器名必须与类名相同。一旦定义返回值类型(包括使用void),构造器就成了方法。
--5.1.3 对象、引用和指针
public class Person { public String name; public int age; public void say(String content) { System.out.println(content); } }
Person p=new Person();
下图显示了Person对象在内存中存储示意图:
下图显示将Person对象赋给一个引用变量的示意图:
栈内存里的引用变量并未真正存储对象的成员变量,对象的成员变量数据实际存放在堆内存中;而引用变量只是指向该堆内存里的对象。
--5.1.4 对象的 this 引用
Java 提供了一个 this 关键字,this 关键字总是指向调用该方法的对象。
1. 构造器中引用该构造器正在初始化对象。
2. 在方法中引用调用该方法的对象。
this 关键字最大的作用就是让类中的一个方法,访问该类里的另一个方法或实例变量。
原方法:
public class Dog { public void jump(){ System.out.println("正在执行jump方法"); } public void run(){ var d = new Dog(); d.jump(); //用对象调用该类中中的其他方法。 System.out.println("正在执行run方法"); } }
主方法:
public class DogTest { public static void main(String[] args) { var dog = new Dog(); dog.run(); } }
this 改写:
public class Dog { public void jump(){ System.out.println("正在执行jump方法"); } public void run(){ this.jump(); //使用this引用调用run()方法的对象。 System.out.println("正在执行run方法"); } }
run()方法简写:
public void run(){ jump(); //this可以省略 System.out.println("正在执行run方法"); }
对于 static 修饰的方法,则可以直接使用类来直接调用该方法,如果在static修饰的方法中使用 this 关键字,则这个关键字就无法指向合适的对象。所以,static 修饰的方法不能用this引用。由于 static 修饰的方法不能使用 this 引用,所以 static 修饰的方法不能访问不使用的 static 修饰的普通成员,因此Java语法规定:静态成员不能直接访问非静态成员。
错误示例:
public class StaticAccessNonStatic{ public void info(){ System.out.println("简单的info方法"); } public static void main(String[] args) { //因为main()方法是静态方法,而info是非静态方法 //调用main()方法的是该类本身,而不是该类的实例 //因此省略的 this 无法指向有效的对象 info(); } }
如果确实需要在静态方法中访问另一个普通方法,则只能重新创建一个对象。
//创建一个对象作为调用者来调用info()方法 new StaticAccessNonStatic().info();
除此之外,this引用也可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用,而不是使用对象来调用的,所以this在构造器中代表该构造器正在初始化的对象。
public class ThisInConstructor{ //定义一个名为foo的成员变量 public int foo; public ThisInConstructor(){ //在构造器定义一个foo变量 int foo = 0; //使用 this 代表构造器正在初始化的对象 //下面代码会把构造器的对象的foo成员变量记为6 this.foo = 6; } public static void main(String[] args) { //所有使用ThisInConstructor创建的对象的foo成员变量都将设为6 System.out.println(new ThisInConstructor().foo); //创建foo=6的匿名对象 } }
当 this 作为对象的默认引用使用时,程序可以像访问普通引用变量一样来访问这个 this 引用,甚至可以把 this 当成普通方法的返回值。
public class ReturnThis(){ public int age; public ReturnThis grow(){ age++; //return this返回调用该方法的对象 return this; } public static void main(String[] args) { var rt = new ReturnThis(); //可以连续调用同一个方法 rt.grow().grow().grow(); //返回值是该类生成的对象,再调用该类的方法 System.out.println("rt的age成员变量值是:"+rt.age); } }
从上面的程序中可以看出,如果再某个方法中把this作为返回值,则可以多次连续调用同一个方法。
5.2 方法详解
--5.2.1 方法的所属性
- 方法不能独立定义,方法只能在类体内定义
- 方法属于类本身或对象
- 永远不能独立执行方法
--5.2.2 方法的参数传递机制
值传递:将实际参数的值的副本传入方法内,但方法本身不会受任何影响。
public class PrimitiveTransferTest{ public static void swap(int a,int b){ //下面定义三行代码实现a、b变量的值交换 //定义一个临时变量来保存a变量的值 var tmp = a; //把 b 的值赋给 a a = b; //把临时变量 tmp 的值赋给 a b = tmp; System.out.println("swap 方法里,a 的值是" + a + "; b的值是" + b); } public static void main(String[] args) { var a = 6; var b = 9; swap(a , b); System.out.println("交换结束后,a 的值是" + a + "; b的值是" + b); } }
引用类型值传递:
class DataWrap{ int a; int b; } public class ReferenceTransferTest{ public static void swap(DataWrap dw){ var tem = dw.a; dw.a = dw.b; dw.b = tem; System.out.println("swap方法里,a成员变量的值是" + dw.a + "; b 成员变量的值是" +dw.b); } public static void main(String[] args) { var dw = new DataWrap(); dw.a = 6; dw.b = 9; swap(dw); System.out.println("交换结束后,a成员变量的值是" + dw.a + "; b 成员变量的值是" +dw.b); } }
上面的程序可以看出如果是引用类型传递值,成员变量的值在堆内存中是会改变的。
--5.2.3 形参个数可变的方法
public class Varargs { //定义了形参个数可变的方法 public static void test(int a,String... books){ //... 代表可接受多个参数 //books 被当成数组处理 for(var tmp : books){ System.out.println(a); } } public static void main(String[] args) { test(5,"fengkuang java","qingliang java"); //test(23,new String[] {"fengkuang java","qingliang java"}); //数组遍历 } }
--5.2.4 递归方法
public class Recursive{ public static int fn(int n){ if(n == 0){ return 1; } else if (n == 1){ return 4; } else { //方法中调用它自身,就是方法递归 return 2 * fn(n - 1) + fn(n - 2); } } public static void main(String[] args) { //输出fn(10)的结果 System.out.println(fn(10)); } }
--5.2.5 方法重载
public class Overload{ //下面定义了两个test()方法,但方法的形参列表不同 //系统可以区分这两个方法,这被称为方法重载 public void test(){ System.out.println("无参数"); } public void test(String msg){ System.out.println("重载的test方法 " + msg); } public static void main(String[] args) { var ol = new Overload(); //调用test()时没有传入参数,因此系统调用上面没有参数的test()方法 ol.test(); //调用test()时传入了一个字符串参数 //因此系统调用上面带一个字符串参数的test()方法 ol.test("hello"); } }
5.3 成员变量和局部变量
--5.3.1 成员变量和局部变量
package homework; class Person{ //定义一个实例变量 public String name; //定义一个类变量 public static int eyeNum; } public class PersonTest{ public static void main(String[] args){ //第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出0 System.out.println("Person的 eyeNum类变量值:"+Person.eyeNum); //创建Person对象 var p = new Person(); //通过Person对象的引用p来访问Person对象name实例变量 //并通过实例访问eyeNum类变量 System.out.println("p变量的name变量值是:"+p.name+ "p对象的eyeNum变量值是:"+p.eyeNum); //直接为name实例变量赋值 p.name = "孙悟空"; //通过p访问eyeNum类变量,依然是访问Person的eyeNum类变量 p.eyeNum = 2; //再次访问 System.out.println("p变量的name变量值是:"+p.name+ "p对象的eyeNum变量值是:"+p.eyeNum); //前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2 System.out.println("Person的eyeNum类变量值:"+Person.eyeNum); var p2 = new Person(); //p2访问的eyeNum类变量依然引用Person类的,因此依然输出2 System.out.println("p2对象的eyeNum类变量值:"+p2.eyeNum); } }
从上面程序来看,成员变量无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化。
Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this或类名来调用。
public class VariableOverrideTest{ //定义一个name实例变量 public String name = "李刚"; //定义一个price类变量 private static double price = 78.0; //主方法,程序的入口。 public static void main(String[] args) { //方法里的局部变量,局部变量覆盖成员变量 var price = 65; //直接访问price变量,将输出price局部变量的值:65 System.out.println(VariableOverrideTest.price); //运行info方法 new VariableOverrideTest().info(); //匿名类 } public void info(){ //方法里的局部变量,局部变量覆盖成员变量 var name = "孙悟空"; //直接访问name变量,将输出name局部变量的值:"孙悟空” System.out.println(name); //使用this来作为name变量限定 //将输出name : 李刚 System.out.println(this.name); } }
--5.3.2 成员变量的初始化和内存中的运行机制
person 类在上方。
var p1 = new Person(); var p2 = new Person(); p1.name = "张三"; p2.name = "孙悟空"; p1.eyeNum = 2; p2.eyeNum = 3;
--5.3.3 局部变量的初始化和内存中的运行内存中的运行机制
局部变量定义后(未分配内存),必须经过显示初始化后才能使用,系统不会为局部变量执行初始化。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。
栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。
--5.3.4 变量的使用规则
...
--5.4 隐藏和封装
--5.4.1 理解封装
封装是面向对象的三大特征之一(另外两个是继承和多态),它指的是对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
--5.4.2 使用访问控制符
private | default | protected | public | |
同一个类中 | √ | √ | √ | √ |
同一个包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围内 | √ |
提示:如果一个Java源文件里定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如何一个Java源文件里定义了一个public修饰的类,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义一个public修饰的类,则这个源文件的文件名必须与public修饰的类的类名相同。
public class Person{ //使用private修饰成员变量,将这些成员变量隐藏起来 private String name; private int age; //提供方法来操作name成员变量 public void setName(String name) { //执行合理性校验,要求用户名必须在2~6位之间 if (name.length() > 6 || name.length() < 2) { System.out.println("您设置的人名不符合要求"); return; } else { this.name = name; } } //提供方法来操作age成员变量 public void setAge(int age){ //执行合理性校验,要求年龄必须在0~100之间 if(age > 100 || age < 0) { System.out.println("您设置的年龄不合法"); return; } else { this.age = age; } } public int getAge(){ return this.age; } }
定义了Person类后,类中的name和age两个成员变量只有在Person类内才可以操作和访问,在Person类之外,只能通过各自对应的setter和getter方法来操作和访问他们。
--5.4.3 package、import、import static
1. import com.lfw.dao,* 语句中的星号(*)只能代表w类,不能代表包。
2. Java默认为所有源文件导入java.lang 包下的所有类(eg:String , System)。
JDK1.5以后增加了一种静态导入的方法,它用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。
例1:
import static package.subpackage...ClassName.fieldName|methodName;
上面语法导入 package.subpackage...ClassName 类中名为 fieldName 的静态成员变量或者名为methodName 的静态成员方法。
例2:
import static package.subpackage...ClassName.*;
导入指定类的全部静态成员变量、方法的语法格式。
总结:import 与 import static 的作用:使用import可以省略写包名;而使用import static则可以连类名都省略。
import static java.lang.System.*; import static java.lang.Math.*; public class StaticImportTest{ public static void main(String[] args) { //out是java.lang.System类的静态成员变量,代表标准输出。 //PI是java.lang.Math类的静态成员变量,表示Π常量 out.println(PI); //直接调用Math类中的sqrt静态方法 out.println(sqrt(256)); } }
5.5 深入构造器
--5.5.1 使用构造器执行初始化
构造器最大的用处就是在创建对象时执行初始化。
--5.5.2 构造器的重载
public class Apple{ public String name; public String color; public double weight; public Apple(){} //两个参数的构造器 public Apple(String name,String color){ this.name = name; this.color = color; } //三个参数的构造器 public Apple(String name,String color,double weight){ //通过this调用另一个重载的构造器初始化代码 this(name,color); //下面this引用该构造器正在初始化的Java对象 this.weight = weight; } }
上面的Apple类里包含了三个构造器,其中第三个构造器通过this来调用另一个重载构造器的初始化代码。程序中this(name,color);调用表明调用该类另一个带两个字符串的构造器。
5.6 类的继承
--5.6.1 继承的特点
Java的继承通过extends关键字来实现,且每个类最多只有一个直接父类。
public class Fruit{ public double weight; public void info(){ System.out.println("我是一个水果!重"+ weight + "g! "); } }
子类Apple:
public class Apple extends Fruit{ public static void main(String[] args) { //创建Apple对象 var a = new Apple(); //访问父类成员变量 a.weight = 56; //调用父类方法 a.info(); } }
--5.6.2 重写父类的方法
public class Bird{ //Bird类的fly()方法 public void fly(){ System.out.println("我在天空中飞翔..."); } }
子类Ostrich:
public class Ostrich extends Bird{ //重写Bird类的fly方法 public void fly(){ System.out.println("我只能在地上奔跑..."); } public static void main(String[] args) { //创建Ostrich对象 var os = new Ostrich(); //执行Ostrich对象的fly()方法,将输出“我只能在地上奔跑。。。” os.fly(); } }
这种子类包含与父类同名方法的现象被称为方法重写,也成为方法覆盖。
注意:覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
class BaseClass{ public static void test(){...} } class SubClass extends BaseClass{ public void test(){...} }
以上写法是错误的。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法。但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super或者父类类名作为调用者来调用被覆盖的方法。
如果父类方法具有private访问权限,则该方法对其子类时隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。如下:
class BaseClass{ //test()方法是private访问权限,子类不可以访问该方法 private void test(){...} } class SubClass extends BaseClass{ //此处并不是方法重写,所以可以增加static关键字 public static void test(){...} }
java中方法的重写的两同两小一大原则
--5.6.3 super限定(调用父类被覆盖的实例方法或变量)
super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中。
在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量。如下:
class BaseClass{ public int a = 5; } public class SubClass extends BaseClass{ public int a = 7; public void accessOwner(){ System.out.println(a); } public void accessBase(){ //通过super来限定访问从父类继承得到的a实例变量 System.out.println(super.a); } public static void main(String[] args) { var sc = new SubClass(); sc.accessOwner(); //输出7 sc.accessBase(); //输出5 } }
因为子类中定义与父类中同名的实例变量并不会完全覆盖父类中定义的实例变量,他只是简单的隐藏了父类中的实例变量,所以出现了如下情形。
class Parent{ public String tag = "疯狂Java讲义"; } class Derived extends Parent { //定义一个私有的tag实例变量来隐藏父类的tag实例变量 private String tag = "JavaEE..."; //非重写关系,不能用super调用。 } public class HideTest{ public static void main(String[] args) { var d = new Derived(); //程序不可访问d的私有变量tag,所以下面语句将引起编译错误 System.out.println(d.tag); //原因:在另一个类中 //将d变量显式的向上转型为Parent后,即可访问tag实例变量 //程序将输出:"疯狂Java讲义" System.out.println(((Parent)d).tag); } }
--5.6.4 调用父类构造器
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始代码,类似于前面所介绍的一个构造器调用另一个重载的构造器。
在一个构造器中调用另一个重载的构造器使用this调用来完成,在子类构造器中调用父类构造器使用super调用来完成。
class Base{ public double size; public String name; public Base(double size,String name){ this.size = size; this.name = name; } } public class Sub extends Base{ public String color; public Sub(double size,String name,String color){ //通过super调用来调用父类构造器的初始化过程 super(size,name); this.color = color; } public static void main(String[] args) { var s = new Sub(5.6,"测试对象","红色"); System.out.println(s.size + "--" + s.name + "--" + s.color); } }
不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器有如下几种情况。
① 子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类构造器。
② 子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时也会先调用父类构造器。
③ 子类构造器执行体中即没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
class Creature{ public Creature(){ System.out.println("Creature无参数构造器"); } } class Animal extends Creature{ public Animal(String name){ System.out.println("Animal 带一个参数的构造器,"+"该动物的name为" + name); } public Animal(String name,int age){ //使用this调用同一个重载的构造器 this(name); System.out.println("Animal 带两个参数的构造器,"+"其age为" + age); } } public class Wolf extends Animal{ public Wolf(){ //显示调用父类有两个参数的构造器 super("灰太狼",3); System.out.println("Wolf无参数的构造器"); } public static void main(String[] args) { new Wolf(); //这里会有四个构造器调用 } }
5.7 多态
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
--5.7.1 多态性
class BaseClass{ public int book = 6; public void base(){ System.out.println("父类的普通方法"); } public void test(){ System.out.println("父类的被覆盖的方法"); } } public class SubClass extends BaseClass{ //重新定义一个book实例变量隐藏父类的book实例变量 public String book = "Java EE..."; public void test(){ System.out.println("子类的覆盖父类的方法"); } public void sub() { System.out.println("子类的普通方法"); } public static void main(String[] args) { //下面编译时类型和运行时类型完全一样,因此不存在多态 BaseClass bc = new BaseClass(); //输出6 System.out.println(bc.book); bc.base(); bc.test(); //下面编译时类型和运行时类型完全一样,因此不存在多态 SubClass sc = new SubClass(); //输出"Java EE..." System.out.println(sc.book); //下面调用将执行从父类继承到base()方法 sc.base(); //下面调用将执行当前类的test()方法 sc.test(); //下面编译时类型和运行时类型不一样,多态发生 BaseClass ploymophicBc = new SubClass(); //编译时是BaseClass类型,运行时是SubClass. //输出6 ———— 表明访问的是父类对象的实例变量 System.out.println(ploymophicBc.book); //下面调用将执行从父类继承到的base()方法 ploymophicBc.base(); //下面调用将执行当前类的test()方法 ploymophicBc.test(); //因为ploymophicBc的编译时类型是BaseClass //BaseClass类没有提供sub()方法,所以下面代码编译时会出现错误 //ploymophicBc.sub(); } }
注意:引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。因此,编写Java代码时,引用变量只能调用声明该变量时所用类里包含的方法。例如,通过Object p = new Person()代码定义一个变量p,则这个p只能调用Object类的方法,而不能调用Person类里定义的方法。
--5.7.2 引用变量的强制类型转换
编写Java程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型,强制类型转换需要借助于类型转换运算符。如下程序展示:
public class ConversionTest{ public static void main(String[] args) { var d = 13.4; var l =(long) d; System.out.println(l); var in = 5; //试图把一个数值类型的变量转换为Boolean类型,下面代码编译出错 //编译时会提示:不可转换的类型 var b = (boolean) in; Object obj = "Hello"; //obj 变量的编译时类型为Object,Object与String存在继承关系,可以强制类型转换 //而且obj变量的实际类型的是String,所以运行时也可通过 var objStr = (String) obj; System.out.println(objStr); //定义一个objPri变量,编译时类型为Object,实际类型为Integer Object objPri = Integer.valueOf(5); //objPri变量的编译时类型为Object,objPri的运行是类型为Integer //Object与Integer存在继承关系 //可以强制类型转换,而objPri变量的实际类型是Integer //所以下面代码运行时引发ClassCastException异常 var str = (String) objPri; } }
代码完善:
在进行强制类型转换之前,先用instanceof运算符判断是否可以成功转换,从而避免出现ClassCastException异常。
if(objPri instanceof String){ var str = (String)objPri; }
--5.7.3 instanceof 运算符
instanceof 运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true , 如果不是,则返回false。
public class InstanceofTest{ public static void main(String[] args) { //声明hello时使用Object类,则hello的编译类型是Object //Object是所有类的父类,但hello变量的实际类型是String Object hello = "Hello"; //String 与 Object 类存在继承关系,可以进行instanceof运算。返回true System.out.println("字符串是否是Object类的实例:" + (hello instanceof Object)); System.out.println("字符串是否是String类的实例:" + (hello instanceof String)); //Math 与 Object 类存在继承关系,可以进行instanceof运算。返回false System.out.println("字符串是否是Math类的实例:" + (hello instanceof Math)); //可以编译 //String实现了Comparable接口,所以返回true System.out.println("字符串是否是Comparable接口的实例:" + (hello instanceof Comparable)); var a = "Hello"; //String 与 Math 类没有继承关系,所以下面代码无法通过编译 System.out.println("字符串是否是Math类的实例:" + (a instanceof Math)); } }
5.8 继承和组合
--5.8.1 使用继承的注意点
...
--5.8.2 利用组合实现复用
继承实现类复用:
class Animal{ private void best(){ System.out.println("心脏跳动..."); } public void breathe(){ best(); System.out.println("吸一口气,吐一口气,呼吸中..."); } } //继承Animal,直接复用父类的breathe()方法 class Bird extends Animal{ public void fly(){ System.out.println("我在天空中自由的飞翔..."); } } //继承Animal,直接复用父类的breathe()方法 class Wolf extends Animal{ public void run(){ System.out.println("我在陆地上快速奔跑..."); } } public class InheritTest{ public static void main(String[] args) { var b = new Bird(); b.breathe(); b.fly(); var w = new Wolf(); w.breathe(); w.run(); } }
组合实现复用:(无继承)
class Animal{ private void best(){ System.out.println("心脏跳动..."); } public void breathe(){ best(); System.out.println("吸一口气,吐一口气,呼吸中..."); } } class Bird{ //将原来的父类组合到原来的子类,作为子类的一个组合成分 private Animal a; public Bird(Animal a){ this.a = a; } //重新定义一个自己的breathe()方法 public void breathe(){ //直接复用Animal提供的breathe()方法来实现Bird的breathe()方法 a.breathe(); } public void fly(){ System.out.println("我在天空自在的飞翔..."); } } class Wolf{ // 将原来的父类组合到原来的子类,作为子类的一个组合部分 private Animal a; public Wolf(Animal a){ this.a = a; } //重新定义一个自己的breathe()方法 public void breathe(){ //直接复用Animal提供的breathe()方法来实现Bird的breathe()方法 a.breathe(); } public void run(){ System.out.println("我在陆地上的快速奔跑..."); } } public class CompositeTest{ public static void main(String[] args) { //此时需要显示创建被组合的对象 var a1 = new Animal(); var b = new Bird(a1); b.breathe(); b.fly(); //此时需要显示创建被组合的对象 var a2 = new Animal(); var w = new Wolf(a2); w.breathe(); w.run(); } }
5.9 初始化块
--5.9.1 使用初始化块
初始化块语法格式:
[修饰符]{ //初始化块的可执行性代码 ... }
初始化块的修饰符只能是static,使用static修饰的初始化块被称为类初始化块(静态初始化块),没有 static 修饰的初始化块被称为实例初始化块(非静态初始化块)。初始化块里的代码可以包含任何可执行语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。
public class Person{ //下面定义一个实例初始化块 { var a = 6; if(a > 4){ System.out.println("Person实例初始化:局部变量a的值大于4"); } System.out.println("Person 的实例初始化块"); } //定义第二个实例初始化块 { System.out.println("Person的第二个实例初始化块"); } //定义无参数构造器 public Person(){ System.out.println("Person 类的无参数构造器"); } public static void main(String[] args) { new Person(); } }
执行结果:
Person实例初始化:局部变量a的值大于4 Person 的实例初始化块 Person的第二个实例初始化块 Person 类的无参数构造器
注意:
虽然Java允许一个类里定义2个实例初始化块,但这并没有任何意义。因为实例初始化块是在创建Java对象时隐式执行的,而且他们总是全部执行,因此完全可以把多个实例初始化块合并成一个实例初始化块,从而可以让程序更加简洁。
--5.9.2 实例初始化块和构造器
从某种程度上来看,实例初始化块是构造器的补充,实例初始化块总是在构造器执行之前执行。系统同样可以使用实例初始化块来进行对象的初始化操作。
与构造器不同的是,实例初始化块是一段固定执行的代码,他不能接受任何参数。因此实例初始化块对同一个类的所有对象所进行的初始化完全相同。故因此产生构造器无参构造提取到实例初始化块中。
--5.9.3 类初始化块(静态初始化块)
类初始化块使用了static修饰符,与类相关的,用于对整个类进行初始化处理,通常用于对类变量执行初始化处理。类初始化块不能对实例变量进行初始化处理。
class Root{ static{ System.out.println("Root的类初始化块"); } { System.out.println("Root的实例初始化块"); } public Root(){ System.out.println("Root的无参数构造器"); } } class Mid extends Root{ static { System.out.println("Mid的类初始化块"); } { System.out.println("Mid的实例初始化块"); } public Mid(){ System.out.println("Mid的无参数构造器"); } public Mid(String msg){ //通过this 调用同一类中重载的构造器 this(); System.out.println("Mid的带参数构造器,其参数值:" +msg); } } class Leaf extends Mid{ static { System.out.println("Leaf的类初始化块"); } { System.out.println("Leaf的实例初始化块"); } public Leaf(){ //通过super调用父类中有一个字符串参数的构造器 super("疯狂Java讲义"); System.out.println("执行Leaf的构造器"); } } public class Test{ public static void main(String[] args) { new Leaf(); new Leaf(); } }
运行结果可以反映调用顺序:
Root的类初始化块 Mid的类初始化块 Leaf的类初始化块
--类初始化阶段 Root的实例初始化块 Root的无参数构造器 Mid的实例初始化块 Mid的无参数构造器 Mid的带参数构造器,其参数值:疯狂Java讲义 Leaf的实例初始化块 执行Leaf的构造器
--对象初始化阶段
Root的实例初始化块 Root的无参数构造器 Mid的实例初始化块 Mid的无参数构造器 Mid的带参数构造器,其参数值:疯狂Java讲义 Leaf的实例初始化块 执行Leaf的构造器