Java程序设计7——面向对象三大特征:封装、继承、多态
1.封装
封装(encapsulation)是面向对象三大特征之一(另外两个是继承和多态(也就是覆写和重载)),它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
前面已经说明了我们可以通过创建对象的方式来访问类和对象的属性,但这种访问可能是有问题的。比如将某个Person的age属性直接设为1000而不加检查就违背了事实。通过方法来访问属性不仅可以进行设置,而且可以对属性进行检查校验。
类是对象的模板,由于很多对象可能都是共用一个类,而对象的方法调用有可能改变类的属性值或者别的什么方法参数,因此在使用时,禁止直接调用类的属性和方法,必须通过间接调用。具体来说就是通过set和get方法来调用。只要是被封装的属性和方法都必须通过set和get方法进行初始化和取值
对一个类或对象实现合适的封装可以实现如下目的
1. 隐藏类的实现细节
2. 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问。
3. 可进行数据检查,从而有利于保证对象信息的完整性。
4. 便于修改,提高代码的可维护性。
为了实现良好的封装,需要从下面考虑
1. 对象的属性和实现细节隐藏起来,不允许外部直接访问。
2. 将方法暴露出来,让方法来操作或访问这些属性。
因此,封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。也就是说封装是有级别的,封装级别"深"的外界就无法直接访问。封装级别在Java里面是通过访问控制符来实现的。
封装性使用语法如下:
private 属性类型 属性名称 private 方法返回值 方法名称(参数类型 参数列表){}
用eclipse编写代码时候,定义类的属性并封装后,可以按shift+alt+s键,弹出菜单,选择生成setter和getter方法。
采用封装的办法不仅有利于安全性,而且还可以对实例初始化时,进行参数检查。
1.1 封装级别
Java提供了三个访问控制符:private,protected,public,代表三种访问控制级别,另外还有一种默认情况就是不加,但是并不是这个控制符是default,很多书籍上都用default来表达默认这个控制级别,但是会很容易给人造成一种错觉:default是除了在switch语句中用到外,还可以当做访问控制符。这个是不正确的。
访问控制级别顺序:private 不写控制级别 protected public,由紧到松
private(类访问权限):类的成员(属性和方法)只能在成员所在的类内被访问,属性通常会用这个访问控制符来修饰,把属性隐藏在类内部。
无控制符(包访问权限):如果类或者类的成员没有任何访问控制符修饰,这个类的成员或者类本身可以被相同包下的其他类访问。
protected(子类访问权限):这个访问控制级别等于默认级别外加不同包下的子类访问。通常情况下,如果使用protected来修饰方法,通常是希望其子类来重写这个方法。
public(公共访问权限):这个级别访问没有限制,任何包的类、方法都可以访问。
封装属性主要目的是通过方法来调用以便进行校验。
通过上面的介绍可以知道访问控制符是用于控制类的属性和方法的是否可以被其他类访问的。由于Java不支持方法的嵌套定义,所以方法必定是属于类的,而变量分为成员变量和局部变量,成员变量属于类和对象,局部变量属于方法,并且局部变量的作用域仅仅限于方法或代码块(流程控制语句),肯定不可能被其他类访问,因此局部变量不支持控制符修饰。
注:如果一个Java源文件里定义的所有类都没有使用public修饰,则这个Java源文件的文件名不受源文件里面的类名一致的限制,也就是可以取任何合法的文件名,但是如果这个源文件里面有public修饰的类,则这个源文件名必须和public类的类名一致,这样就很容易得到一个结论,一个源文件里面只能有一个public类,否则源文件名就没法命名了。
package chapter5; public class Person { //创建对象属性 private String name; private int age; //设置对象姓名属性,并进行访问控制 public void setName(String name){ if((name.length() > 0)&&(name.length() < 9)){ this.name = name; }else{ System.out.println("名字长度不符合"); } } //返回对象姓名属性 public String getName(){ return this.name; } public void setAge(int age){ if((age > 0)&&(age < 150)){ this.age = age; }else{ System.out.println("请输入正确的年龄"); } } public int getAge(){ return this.age; } public static void main(String[] args){ Person p = new Person(); //因为name和age已经被封装隐藏了,所以不能直接访问,需要调用方法访问以便校验 //p.name = "淑媛"; p.setName("淑媛"); System.out.println("姓名:" + p.getName()); p.setAge(23); System.out.println("年龄:" + p.getAge()); } }
上面代码里的setName(),getName(),setAge(),getAge(),不知道被哪个人才称为setter和getter方法。这两种方法用于设置和取得类或对象的属性。通过这两种方法允许程序设计者增加自己的控制逻辑,以防止不符合实际情况的设置。
如果属性名不是static,那么相应的setter和getter方法也不能是static,因为进行this调用。
注意:
一个.java源文件里面最多只有一个public类,并且理论上可以有无限多个非public类,但是从良好的程序设计风格上来说,最好一个源文件对应一个类,并且类的声明和类的实现要分开。
每一个类里面都可以有主方法,但最关键的是程序运行时,由哪个主方法发起程序运行。
一个类常常就是一个小的模块,我们应该只让这个模块公开必须让外界知道的内容,而隐藏其他一切内容。进行程序设计时,应尽量避免一个模块直接操作和访问另一个模块的数据(属性、常量等),模块设计追求高内聚(尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预)、低耦合(仅暴露少量方法给外部使用)。高内聚和低耦合是对应的,高内聚带来的必然是低耦合。
关于访问控制符的使用,存在如下几条原则:
1.类里的绝大部分属性都应该使用private修饰,除了一些static修饰的、类似全局变量的属性,才可能考虑使用public修饰。除此之外,有些方法只是用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
2.如果某个类主要用作其他类的父类,该类里包含的大部分方法可能仅希望背其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
3.希望暴露出给其他类自由调用的方法应该使用public修饰,因此,类的构造器通过使用public修饰,暴露给其他类中创建该类的对象。因为顶级类通常都希望杯其他类自由使用,所以大部分顶级类都使用public修饰。
关于封装的说明
1、必须注意的是,类的全部属性都必须使用封装,然后通过setter和getter方法访问。
2、private声明的属性和方法只能在类的内部被直接调用,类的外部及子类禁止调用。如果真要调用,只能先实例化,然后通过setter和getter方法调用。
3、类内部调用方法,可以在方法前加上this关键字,如this.tell()。表示这个方法this是类内部的,可以直接调用。
1.2 package和import
Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,我们应该在Java源程序的第一个非注释行放如下格式的代码:
package 包名
一旦在Java源文件中使用了这个package语句,则意味着该源文件里定义的所有类都属于这个包。位于包中的每个类的完整类名是包名和类名的组合,如果其他人需要使用该包下的类,则需要使用完整的类名。
Java语法只要求包名是有效的标识符即可,但从可读性角度来看,包名应该全部由小写字母组成,并且使用Internet域名倒写来作为包名。例如cnblog.com,则关于cnblog的所有类可以放在com.cnblog的包及其子包下面。
javac -d 路径名 java文件
上面的命令用于保存class文件位置,路径名如果是一个点·,则表示编译后的class文件在被编译的java文件所在的文件夹。
classpath用于配置存放class文件的路径,可以在环境变量里面配置。
可以将java源文件和对应的class文件分开放,目录层次深度一样。所在的包名也一样。例如可以在这个包下放class文件F:\Personal Study\CodeLibrary\JavaExcercise\Hadoop\bin\chapter1\TestGetPost.class
而在这个包下方java源文件F:\Personal Study\CodeLibrary\JavaExcercise\Hadoop\src\chapter1\TestGetPost.java
由于一个源文件只能指定一个包,所以只能包含一条package语句,该源文件中可以定义多个类,则这些类将全部位于该包下。
import关键字
如果创建处于其他包下类的实例,则在调用构造器时也需要使用包前缀。例如需要lee.TestHello类中创建lee.sub.Apple类的对象,则需要如下代码。
lee.sub.Apple apple = new lee.sub.Apple();
显然上面写法是比较麻烦的,为了简化这样的编码,Java引入了import关键字,import可以向某个java文件导入指定包层次下的某个类或全部的类,import语句放在package语句后、类定义之前。一个java源文件只能包含一个package语句,可以包含多个import语句。可以用星号匹配所有的类,注意只是匹配类,而不能代表包。
一旦使用了import导入指定的类,就不必使用类的全名来创建实例,直接用类简单名来创建即可。
java默认所有的源文件导入java.lang包下的所有类,因此java程序使用的String、System类时都无需使用import语句来导入这些类,但对于介绍数组提到的Arrays类,其位于java.util包下,则必须使用import语句来导入该类。
但是在一些极端情况下:import语句也很无奈,就是说import导入的多个包下面可能包含相同名称的类,这样的话如果用简单的类名称来创建对象,系统就不知道用哪个包下的类来创建对象。比如说java.sql和java.util两个包下面都有Date类,如果需要使用Date类就必须使用类的全名来创建实例,否则系统就会糊涂。当然如果系统糊涂了,就说明程序设计者没有搞清楚用哪个包,不是系统问题。
注意:两种导包方式
1.import 包名.子包名...类名 //手动加载所需要的类
2、import.包名.子包名.*//自动加载JVM所需要的类
备注:上面两种方式导入的方式使用时性能是一样的,没有区别。使用2时自动加载所需的类,不需要的类不会被加载。
1.3 Java的常用包
Java的核心类都放在java这个包下及其子包下,java扩展的许多类都放在javax包及其子包下。这些实用类也就是API。
包名称 作用
java.lang JAVA的基本包,使用时侯自动导入
java.reflect 反射包
java.util 工具包,一些常见的类库和日期包都在此,掌握此包一些常见的设计思路会很好理解
java.text Java文本处理类库
java.sql 数据库操作包,提供数据库操作及接口
java.net 完成网络编程
java.io 输入输出处理
java.awt 构成输入输出窗口工具包
javax.swing 建立图形用户界面
class文件的打包:jar命令的使用
一套系统里面包含很多java文件,编译后会对应很多class文件,这样很麻烦,通常会将这些class文件进行打包压缩成.jar文件。使用命令jar。
使用语法:jar -cfv 文件名.jar 包名 如:jar -cvf abc.jar abc
查看jar包详细信息:jar -tvf jar文件名 如:jar -tvf abc.jar
2. 类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段,Java的继承是单继承,每一个子类只有一个直接父类。
2.1 继承的特点
Java的继承通过extends关键字来实现,实现继承的类称为子类,被继承的类称为父类,有的也称为基类、超类(记得真是超累)。父类和子类的关系是一般和特殊的关系。就像水果和苹果的关系,苹果继承了水果的特点,苹果是水果的子类,苹果是一种特殊的水果。子类比父类有更多的属性。 使用语法:
修饰符 class 子类 extends 父类{};
extends关键字能够减少类似的类代码冗余,比如说一个类:人与一个类:学生之间有很多类似的属性,那么可以通过继承的方式来减少代码冗余。extends关键字英文意思是扩展的意思,就是说子类扩展了父类的一些属性。国内翻译成继承(又是一个奇葩~~)。值得指出的是:Java的子类不能获得父类的构造器。也就是说不能在子类创建对象来调用父类的方法,因为子类本身已经继承了父类的属性和方法。 Java摒弃了C++中难以理解的多继承特征,也就是每个类最多只有一个直接父类,超过一个类将引起编译错误。
一个继承的例子:
父类: package chapter5; public class Fruit { double weight; public void info(){ System.out.println("我是一个水果!重" + weight + "g!"); } } 子类: package chapter5; public class Apple extends Fruit{ public static void main(String[] args){ //创建Apple对象 Apple apple = new Apple(); apple.weight = 5; apple.info(); } }
上面的Apple类本来只是一个空类,它只包含了一个main方法,但程序中创建了Apple对象之后,可以访问Apple对象的weight属性和info方法,这表明Apple对象有了weight属性和info方法。 注意:类里面包含成员变量和成员方法,但main方法不属于成员方法,它是由JVM调用的,不属于程序设计者自己的类。我们可以这样想一个普通类里面包含的是属性和方法,那么肯定有一个方法是特殊的,这个方法需要是程序的执行起始和结束点。这个方法就被设计成main方法。但Java不是面向对象的吗?应该有一个主类吧。假设主类是Main 怎样调用main方法?如果main方法不是static的,则需要Main m = new Main();然后使用m.main(),如果是这样的话main()方法的细节该在哪里设计呢?程序的起始和结束点又该在哪里呢?显然main方法需要设置成static的,由JVM来调用就好了。不必创建主类。所以说虽然main方法可以放在类中,但是main方法并不属于这些类的。假如在Fruit类里面有一个主方法,在Apple类里面也有主方法,在Apple类里面创建一个对象的时候,这个Apple类对象显然不会把main方法也搞来,然后进行调用。main方法不属于任何一个类的成员方法,而是由JVM来调用的。
如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有Java对象都可以调用java.lang.Object类所定义的实例方法。
2.2 重写父类方法
子类总是以父类为基础,额外增加新的属性和方法。不仅如此,子类还可以重写父类的方法。例如鸟类都包含了飞翔的方法,其中鸵鸟是一种特殊的鸟类,是鸟的子类,因此它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合鸵鸟,为此,鸵鸟需要重写鸟类的方法。就是同一种方法表现出不同的行为。
父类: package chapter5; public class Bird { public void fly(){ System.out.println("我在天空里自由自在地飞翔^@^"); } } 子类: package chapter5; public class Ostrich extends Bird{ /** * 覆写父类方法 */ public void fly(){ System.out.println("我只能在地上奔跑。。。"); } public static void main(String[] args){ Ostrich os = new Ostrich(); os.fly(); } }
这种子类包含父类同名方法现象被称为方法重写或者覆写也被称为覆盖。可以说子类重写/覆写/覆盖了父类的方法。 方法重写要遵循"两同两小一大"规则,"两同"即方法名相同、形参列表相同(形参个数、类型、顺序、形参名均相同),"两小"指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常应比父类方法声明抛出的异常类更小或相等。"一大"指的是子类方法的访问权限应比父类方法更大或相等,尤其需要指出的是,覆写方法和被覆写的方法要么都是类方法,要么都是实例方法,不能一概是类方法,一个是实例方法。也就是要么都是static方法,要么都不带static。 下面的代码将会引起编译错误
class Bird{ public static void fly(){ } } class Ostrich extends Bird{ public void fly(){ } }
下面代码展示了对象的转型
package chapter5; public class Bird { public void fly(String name,double speed){ System.out.println(name + "在天空里" + speed + "速度自由自在地飞翔^@^"); } } package chapter5; public class Ostrich extends Bird{ /** * 覆写父类方法 * @return */ public void fly(String name,double speed){ System.out.println("我只能在地上自由奔跑。。。"); } public void fly(double speed,String name){ System.out.println("我只能在地上奔跑。。。"); } public static void main(String[] args){ //创建一个鸵鸟对象 Ostrich os = new Ostrich(); //鸵鸟对象os有两个方法fly,但是形参列表不一样,两个参数顺序不同 //下面这个fly并非重写方法,就是一个普通的方法 os.fly(3, "鸵鸟"); //下面这两个方法是重写的,子类如果没有重写父类方法,将自然继承父类方法,使用父类方法 os.fly("鸵鸟", 3); //下面使用了向上转型,但依然表现出子类自身的方法, //如果自身没有重写的方法,将调用父类的方法 Bird b = new Ostrich(); b.fly("大鸵鸟", 3); } }
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但还可以在子类方法中调用父类中被覆盖方法。如果需要在子类方法中调用父类中被覆盖方法,使用super(被覆盖的是实例方法)或者父类类名(被覆盖方法是类方法)作为调用者来调用父类中被覆盖方法。
如果父类方法具有private访问权限,则该方法对其子类是隐蔽的,因此其子类无法访问该方法(private 是类内部方法,protected是子类可以访问),也就是无法重写该方法,如果子类中定义了一个与父类private方法具有相同方法名,相同形参列表,相同返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。
下面的代码完全正确
package chapter5; public class Bird { private void fly(String name,double speed){ System.out.println(name + "在天空里" + speed + "速度自由自在地飞翔^@^"); } } package chapter5; public class Ostrich extends Bird{ public void fly(String name,double speed){ System.out.println("我只能在地上自由奔跑。。。"); } public void fly(double speed,String name){ System.out.println("我只能在地上奔跑。。。"); //super.fly("大鸵鸟",4); } public static void main(String[] args){ //创建一个鸵鸟对象 Ostrich os = new Ostrich(); //鸵鸟对象os有两个方法fly,但是形参列表不一样,两个参数顺序不同 //下面这个fly并非重写方法,就是一个普通的方法 os.fly(3, "鸵鸟"); //这个是子类的方法,没有重写 os.fly("鸵鸟", 3); } }
2.3 父类实例的super引用
如果需要在子类方法中调用父类被覆盖的实例方法,可使用super作为调用者来调用父类被覆盖的实例方法。为上面的Ostrich类添加一个方法,这个方法调用Bird中被覆盖的fly方法:
public void fly(String name,double speed){ super.fly("大鸵鸟",4); }
通过上面的方式可以看到通过调用子类的覆写方法来调用父类方法,注意super不能出现在main方法中,因为main方式是static的,而super必须有一个父对象,谁调用super方法,谁就给super赋值为谁的父对象。
注意:在上面的继承中,我们只创建了一个子对象Ostrich对象,哪来的Bird父对象呢?实际上Java程序创建某个类的对象时,系统会隐式创建该类父类的对象,只要有一个子类对象存在则一定存在一个与之对应的父类对象。在子类方法中使用super引用时,super总是指向作为该方法调用者的子类对象所对应的父类对象。super和this引用很像。this总是指向调用this所在方法的对象,而super总是指向调用包含super关键字方法的对象的父对象。
如果在构造器中使用super方法,比如super("大鸵鸟",4);则这个调用也必须放在构造器的首行,当然也只有一条super语句。
2.4 重载和覆写
重载和覆写对应的英文单词分别是overload和override,这两个之间的对比没有多少意义。因为重载发送在同一个类的多个同名方法之间(形参个数类型名称均相同,但顺序不同不是重载,重载一般是方法同名、形参个数不同),而覆写发生在子类和父类的同名方法之间(参数顺序、个数、函数名称全部相同)。它们之间的联系很少,除了二者都是发生在方法之间,并要求方法名相同之外,没有太大相似之处。当然父类方法和子类方法之间也可能发生重载。,因为子类会获得父类方法,如果子类定义了一个父类方法又相同方法名,但参数列表不同的方法,就会形成父类方法和子类方法的重载。 如果子类定义了和父类同名的熟悉,也会发生子类熟悉覆盖父类熟悉的情形。正常情况下,子类定义的方法、子类熟悉直接访问该属性,都会访问到覆盖属性,无法访问到父类被覆盖的属性,但是在子类定义的实例方法可以通过super来访问父类被覆写的属性。如果被覆盖的是类属性,在子类方法中可以通过父类名作为调用者来访问被覆写的属性。 如果子类没有包含和父类同名的属性,则子类可以继承到父类的属性,子类实例方法中访问该属性时,无须显示使用super或父类名作为调用者,直接使用实例调用即可。因此如果我们在某个方法中访问名为a的属性,但没有显示指定调用者,系统查找a的顺序为: 1. 查找该方法中是否有名为a的局部变量,如果有则使用。 2. 查找当前类中是否包含名为a的属性,如果有则使用 3. 查找a的直接父类是否包含名为a的属性,一次上溯a的父类,直到java.lang.Object类,如果最终不能找到名为a的属性,则出现系统编译错误。
也就是说覆写遵从就近原则。
class BaseClass{ public int a = 5; } class SubClass extends BaseClass{ public int a = 7; public void accessOwner(){ System.out.println(a); } public void accessBase(){ //通过super来访问方法调用者对应的父类对象 System.out.println(super.a); } public static void main(String[] args){ SubClass sc = new SubClass(); //直接访问SubClass对象的a属性将会输出7 System.out.println(sc.a); //输出7 sc.accessOwner(); //输出5 sc.accessBase(); } }
2.5 使用super调用父类的构造器
子类不会获得父类的构造器(否则创建的就是父类对象了),但有的时候子类构造器里需要调用父类构造器的初始化代码,就如同前面介绍的一个构造器需要调用另一个重载的构造器一样。 在一个类中,一个构造器调用另一个重载的构造器使用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){ Sub s = new Sub(5.6 , "测试对象" , "红色"); //输出Sub对象的三个属性 System.out.println(s.size + "--" + s.name + "--" + s.color); } }
从上面程序可以看出,使用super调用和使用this调用也很像,区别在与super调用的是父类的构造器,而this调用的是同一个类里面的重载的构造器。因此使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。 不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类的构造器一次。
子类构造器调用父类构造器分如下几种情况: 1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。 2. 子类构造器执行体第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入实参列表调用本类另一个构造器。执行本类中另一个构造器时即会调用父类的构造器。 3. 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参构造器。
不管哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行,不仅如此,执行父类构造器时,系统会再次上溯执行其父类的构造器....以此类推,创建java对象,最先执行的总是java.lang.Object类的构造器。
package chapter5; class Creature{ public Creature(){ System.out.println("Creature无参数的构造器"); } }; class Animal extends Creature{ public Animal(String name){ //没有this和super关键字,该构造器会隐式调用父类的构造器,如果构造器写了,则调用, //如果父类没有提供构造器,则不会调用 System.out.println("Animal带一个参数的构造器,该动物的name为" + name); } public Animal(String name , int age){ //使用this调用同一个重载的构造器 this(name); System.out.println("Animal带2个参数的构造器,其age为" + age); } }; public class Wolf extends Animal{ public Wolf(){ //显式调用父类有2个参数的构造器 super("土狼", 3); System.out.println("Wolf无参数的构造器"); } public static void main(String[] args){ new Wolf(); } };
如果父类没有提供构造器,子类也会调用,但是不会输出内容,因为java.lang.Object类的构造器不输出内容。
package chapter5; class Creature{ // public Creature(){ // System.out.println("Creature无参数的构造器"); // } }; class Animal extends Creature{ public Animal(String name){ //没有this和super关键字,该构造器会隐式调用父类的构造器,如果构造器写了,则调用, //如果父类没有提供构造器,则不会调用 System.out.println("Animal带一个参数的构造器,该动物的name为" + name); } public Animal(String name , int age){ //使用this调用同一个重载的构造器 this(name); System.out.println("Animal带2个参数的构造器,其age为" + age); } }; public class Wolf extends Animal{ public Wolf(){ //显式调用父类有2个参数的构造器 super("土狼", 3); System.out.println("Wolf无参数的构造器"); } public static void main(String[] args){ new Wolf(); } };
总之:创建任何对象都会从该类的最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过this调用了同类中重载的构造器,就会依次执行此父类的多个构造器。
自定义的类一般未有显示调用过java.lang.Object类的构造器,即使显式调用,java.lang.Object类构造器也只有一个默认的可被调用。当系统执行java.lang.Object类的默认构造器时,该构造器的执行体并未输出任何内容,所以通常不会感到调用过java.lang.Object类的构造器。
3.多态
Java引用变量有两个类型:一个是编译时的类型,一个是运行时的类型,编译时的类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就会出现所谓的多态。
3 多态性
看一段代码
package chapter1; class BaseClass{ public int book = 6; public void base(){ System.out.println("父类的一个普通方法"); }; public void test(){ System.out.println("父类的被覆盖的方法"); }; }; public class SubClass extends BaseClass{ //覆盖父实例属性的子实例属性 public String book = "基础西班牙语"; //覆写父类的实例方法 public void test(){ System.out.println("覆写父类的方法"); }; public void sub(){ System.out.println("子类的普通实例方法"); }; public static void main(String[] args){ //创建一个父类对象 BaseClass bc = new BaseClass(); //对象实例是父类的,构造方法也是父类的,当然调用父类实例属性,而不是重写的子类实例属性 System.out.println("父类实例属性: " + bc.book); //对象实例是父类的,构造器也是父类的,调用父类的普通实例方法 bc.base(); bc.test(); //创建一个子类对象 SubClass sc = new SubClass(); //覆写父类的实例属性 System.out.println("子类实例属性: " + sc.book); //调用子实例方法,对于test方法,将会覆写父实例方法 sc.sub(); sc.test(); //创建一个父类的子类实例对象 BaseClass toBc = new SubClass(); //子类实例转型成父类实例,虽然子类属性覆写了父类属性,但会使用父类属性 System.out.println("转型成父类实例,拥有父类属性: " + toBc.book); //子类实例转型成父类实例,父类没有的子类方法,转型后将被丢弃 //sub()方法是子类的,转型成父类后,将被丢弃,因为父类不具有这个方法 //toBc.sub(); //子类实例转型成父类实例,子类覆写父类的方法将被保留给子类对象,这就是多态 toBc.test(); } };
3.1 向上转型
从上面的代码可以看出,如果一个类在没有继承除了java.lang.Object类以外的类外时,创建一个实例变量,那么它将表现出该类的属性和方法行为。而如果继承了除了Object类以外的别的类,该类创建一个属于该类自身的实例,那么对于子类自身的属性、方法子类都可以表现出来,无论是子类自身的还是覆写父类的方法或属性。另一种是子类继承了父类,用子类的构造器创建了一个实例,并向上转型为父类实例,在转型过程中对于属性和方法,如果是子类自身的方法和属性将会被丢弃,如果是子类覆写父类的属性,那么将展示父类的属性;
如果是子类覆写父类的方法,将展示子类覆写父类的方法,这个就是多态。
总之:对于三种实例调用属性、方法即没有继承除了Object类意外的类创建实例、子类创建实例、子类创建属于父类的实例(子类本身就是父类的一种特殊情况),对于属性,实例是属于哪一类,实例就拥有哪些属性,如果是从子类转型的,会丢掉子类以前的属性,而拥有转型后的属性;
对于方法,前两种表现和属性一样,实例属于哪一类,实例就拥有该类的所有方法,无论是自身的还是覆写的,而第三种表现出多态,转型过程中,方法并不丢弃,依然表现出覆写的方法。
也就是说,除了子类转型父类的方法依然表现转型前的方法外(对象的属性不具有多态性),其他情况下,实例属于哪一类就表现哪一类的属性和方法。
3.2 向下转型
前面已经知道,如果子类向上转型成父类,那么将只保留子类覆写父类的方法,子类自身的方法将被丢弃,那么如果想保留自身的方法该怎样呢?这就需要向下转型也就是强制转换。注意强制转换只能是父类向子类转换,不能是没有继承关系的转换。并且向下转换前必须向上转换,否则将会抛出ClassCastException异常。
向下转换:
package chapter1; class BaseClass{ public int book = 6; public void base(){ System.out.println("父类的一个普通方法"); }; public void test(){ System.out.println("父类的被覆盖的方法"); }; }; public class SubClass extends BaseClass{ //覆盖父实例属性的子实例属性 public String book = "基础西班牙语"; public int age = 5; //覆写父类的实例方法 public void test(){ System.out.println("覆写父类的方法"); }; public void sub(){ System.out.println("子类的普通实例方法"); }; public static void main(String[] args){ //创建一个父类的子类实例对象 BaseClass toBc = new SubClass(); //子类实例转型成父类实例,虽然子类属性覆写了父类属性,但会使用父类属性 SubClass toSc = (SubClass)toBc; if(toSc instanceof SubClass){ //属性没有多态性,所以什么样的类型将有什么样类型的属性 System.out.println(toSc.book + toSc.age); //向下转型会保留原有拥有的方法而不会丢弃 toSc.base(); toSc.sub(); //同样会覆写父类实例方法 toSc.test(); } } };
切记:如果要进行向下转型,就必须先进行向上转型,并且向下转型后对于没有被覆写的父类方法依然保留,而对于覆写的方法讲话覆写掉父类的覆写方法,子类的非覆写的方法将依然正常使用。如果不进行向上转型将会报ClassCastException异常。
3.3 检测是否可以进行引用类型转换:instanceof运算符
instanceof运算符的操作数有两个,前一个操作数通常是一个引用类型的变量,后一个操作数通常是一个类或接口(没有实现方法的类),它用于判断前面的对象是否是后面的类或其子类、实现类的实例,如果是,返回true,否则返回false。
在使用instanceof运算符时需要注意:instanceof运算符前面操作数的编译时类型要么(定义时候的类型)与后面的类相同,要么是后面类的父类,否则引起编译错误。
package chapter5; public class TestInstanceOf { public static void main(String[] args){ //所有的类都是Object的子类,hello实际是String的类 Object hello = "Hello"; //hello的编译类型是Object,可以与Object的子类或与Object相同的类进行实例鉴定 System.out.println(hello instanceof String); System.out.println(hello instanceof Object); //Math是Object的子类,hello的编译类型是Object,满足instanceof的使用条件 System.out.println(hello instanceof Math); //定义一个String类对象 String str = "World"; //str的编译类型是String,与另一个操作Math并非父子关系或相同的类,无法使用instanceof //System.out.println(str instanceof Math); } }
使用语法:
类型A 引用变量 引用变量 instanceof 类型A或A的子类
instanceof这个运算符的目的主要是看引用变量是否是和编译类型相同或其子类,以便进行向上或向下转型。所以(type)和instanceof通常是搭配在一起使用的。
3.4 总结多态
多态分为两种情况向上转型和向下转型
在转型中表现出多态的是方法,属性并不表现出多态,转型后的实例是属于什么类,就拥有该类的什么样、所有的属性,不多不少。
而对于方法,向上转型时,除了被覆写的方法外,转型后的实例具有所有当前父类除了覆写方法的所有方法(一定注意看转型后的引用变量是什么类型的);对于向下转型,首先必须要向上转型,向下转型后的实例拥有当前实例所属子类类型的所有方法以及转型前具有的父类除了被覆写的方法所有方法。
无论是向上转型还是向下转型,一定要看实例当前的类型,当前类型如果有覆写或被覆写的方法,那都使用覆写的方法,除此之外向上转型拥有父类所有实例方法(被覆写的除外),向下转型拥有的是子类+父类全部方法(被覆写的除外)。
简单的说,如果A是B的子类,即A extends B
向上转型
B b = new A()
因为实例变量b是B类的,所以b理论上具有B的所有方法和属性,而没有和A相关的东西,但因为多态,b的方法中被覆写的将被剔除,而具有覆写的方法。
向下转型
B b = new A() A a = (A)b
因为实例变量a是A类的,所以a理论上具有a的所有方法和属性,并且又因为a的"前世"是B类型的,所以a具有A的所有属性(没有B的属性,属性没有多态性),和所有A+B方法,但需要剔除掉被覆写的,保留覆写的方法。
3.5 继承与组合
继承是实现类重用的重要手段,但继承带来一个最大的坏处:破坏封装。相比之下,组合也是实现类重用的重要方式,而采用组合方式来实现类重用能提供更好的封装性,介绍继承和组合之间的联系和区别。
继承会严重破坏父类的封装性,前面介绍封装时可知:每个类都应该封装它内部细节和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的属性(内部信息)和方法,从而造成子类和父类的严重耦合。从这个角度来看,父类的实现细节对子类不再透明,子类可以访问父类的属性和方法,并可以改变父类方法的实现细节(通过方法重写的方式来改变父类方法实现),从而导致子类可以恶意篡改父类的方法。例如Ostrich就重写了Bird类的fly方法。从而改变了fly方法的实现细节。
3.5.1 设计父类的规则
为了保证父类良好的封装性,不会被子类随意改变,设计父类通常遵循如下规则:
1. 尽量隐藏父类的内部数据,尽量把父类的所有属性设置成private访问类型,不能让子类直接访问父类的属性。
2. 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符来修饰该方法,如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,可使用protected来修饰该方法。
3. 不要在父类构造器中调用被子类重写的方法。否则父类的这个方法等于是白写了。以后可以知道,接口是更常用的,只提供模板,不提供实际实现方法,由实现类来实现接口。
4.如果想把某些类设置成最终类,即不能当成父类。则可以使用final修饰这个类,例如JDK提供的java.lang.String和java.lang.System类。除此之外,或者使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有构造器都使用private修饰的父类而言,可以另外提供一个静态方法,用于创建该类的实例。
何时需要从父类派生新的子类呢?不仅需要保证子类是一种特殊的父类,而且还需要具备以下两个条件之一:
1.子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类。Person类里面没有提供grade属性,而Student类需要grade属性来保存Student对象就读的年级。
2.子类需要增加自己独有的行为方式(包括新的方法或重写父类的方法)。例如从Person类派生出Teacher类,Teacher类需要增加一个teach方法即教学
3.5.2 组合:一个对象中包含另一个对象
如果需要复用一个类,除了把这个类当作基类来继承之外,还可以把父类当做一个普通的类来调用,就是在形式上把父类嵌入到子类中,这样在类的构成形式上看更加可读。不管是继承还是组合都允许在新类(对于继承就是子类)中直接复用父类的方法。(把父类当成普通的类方式使用)
对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,可以直接访问该子类从父类继承到的方法;而组合则是把旧类对象作为新类的属性嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到嵌入对象的方法。因此通常需要在新类里使用private修饰嵌入旧类对象。
从类复用角度来看,不难发现父类的功能等同于被嵌入类,都将自身方法提供给新类使用;子类以及组合关系里的整体类,都可复用原有类的方法,用于实现自身的功能。
所谓的组合就是将旧类当作普通的类来使用,没什么新奇的。
从形式上看,继承是隐式的使用;而组合是显式的。
package chapter1; class Animal{ private void beat(){ System.out.println("心在跳 情在烧"); } public void breath(){ this.beat(); System.out.println("我在呼吸啊"); } }; class Bird extends Animal{ public void fly(){ System.out.println("飞呀飞"); } }; class Wolf extends Animal{ public void run(){ System.out.println("我是来自北方的狼,我在奔跑"); } }; public class TestInherit { public static void main(String[] args){ Bird bird = new Bird(); //继承父类的方法 bird.breath(); bird.fly(); Wolf wolf = new Wolf(); wolf.breath(); wolf.run(); } }
另一种方式就是组合了,把父类当做一种普通的类,嵌入子类里面(父类就是一种普通的类)
package chapter1; class Animal{ private void beat(){ System.out.println("心在跳 情在烧"); } public void breath(){ this.beat(); System.out.println("我在呼吸啊"); } }; class Bird{ //下面这种定义看起来似曾相识,在创建对象时候我们用到过 //Animal animal = new Animal(),这里面并没有将实例变量指向具体的对象而已 //通过这种方式就可以实现调用父类的方法功能,不必使用继承的方式 private Animal animal; public Bird(Animal animal){ this.animal = animal; } public void breath(){ //使用父类的breath方法 animal.breath(); } public void fly(){ System.out.println("飞呀飞"); } }; class Wolf{ private Animal animal; public Wolf(Animal animal){ this.animal = animal; } public void breath(){ animal.breath(); } public void run(){ System.out.println("我是来自北方的狼,我在奔跑"); } }; public class TestComposite { public static void main(String[] args){ Animal a = new Animal(); Bird bird = new Bird(a); bird.breath(); bird.fly(); Wolf wolf = new Wolf(a); wolf.breath(); wolf.run(); } }
从上面可以看出,两种使用方式是等效的。
继承关系中从多个子类中抽象出其共有父类的过程,类似组合关系里从多个整体类里抽取共同嵌入类的过程;
总之,继承要表达的是一种属于的关系,即is a 的关系,而组合表达的是包含的关系即有的关系has a 的关系。
这两种需要考量使用。比如说Person类和Arm类,显然说Arm类继承Person或反过来都不不大合适,但Person类包含Arm类就比较合适(人有手臂,但人继承手臂就比较牵强)
4 初始化块
Java使用构造器来对单个对象进行初始化块操作,使用构造器先把整个Java对象的状态初始化完成,然后将Java对象返回给程序,从而让该Java对象的信息更加完整。与构造器作用非常类似的是初始化块,它也可以对Java对象进行初始化操作。
初始化块语法:
类初始化代码块(无需创建实例即可初始化) static { //初始化代码 } 实例初始化代码块(需要创建实例时才会初始化) { //初始化代码 }
初始化块一般当作Java类里的第四种成员(属性、方法、构造器),一个类里可以有多个初始化块,初始化块定义在前的先执行,后定义的后执行。
前面已经说了,static关键字修饰的是由类直接调用的,实际上也可以由JVM虚拟机调用。这一点在main方法上得到了证实。
package chapter1; public class TestStatic { //第一个static代码块,先执行 static{ System.out.println("HelloWorld"); }; //第二个代码块再执行 static{ System.out.println("你试着将分手尽量讲的婉转,我只好配合你尽量笑得自然"); } //执行main方法,static代码块执行优先级是优于main方法的 public static void main(String[] args){ System.out.println("你好"); } }
注意:没有static的代码块属于实例初始化代码块,需要创建实例时才能初始化,而static代码块属于类初始化代码块,不必创建实例即可初始化。
package chapter1; public class TestStatic { //第一个static代码块,无需实例创建即可执行 static{ System.out.println("HelloWorld"); }; //普通的代码,需要有实例创建才能执行 { System.out.println("你试着将分手尽量讲的婉转,我只好配合你尽量笑得自然"); } //执行main方法,static代码块执行优先级是优于main方法的 public static void main(String[] args){ // System.out.println("你好"); new TestStatic(); // new TestStatic(); } } 执行结果: HelloWorld 你试着将分手尽量讲的婉转,我只好配合你尽量笑得自然
初始化静态属性和初始化静态代码块优先级是一样的,谁在前谁先初始化,静态初始化高于实例初始化
package chapter1; public class TestStatic { //普通的代码,需要有实例创建才能执行 { System.out.println("你试着将分手尽量讲的婉转,我只好配合你尽量笑得自然"); } //第一个static代码块,无需实例创建即可执行 static{ System.out.println("HelloWorld"); }; //执行main方法,static代码块执行优先级是优于main方法的 public static void main(String[] args){ // System.out.println("你好"); new TestStatic(); // new TestStatic(); } } 执行结果依然是这样: HelloWorld 你试着将分手尽量讲的婉转,我只好配合你尽量笑得自然
本章小结:本章重点讲了类文件及类的结构。包括:
1.类的结构:初始化代码块、属性、方法及构造器,以及这四种成分的详细介绍。
2.属性、方法及构造器的封装级别、this及super调用
3.方法之间的两种方式传参
4.面向对象的三大特征:封装、继承和多态(就是对象的类型转换)
5.方法的重载和覆写