类、超类和子类
定义子类
使用extends表示继承,表明正在构造的新类派生于一个已存在的类
public class Manager extends Employee{
}
已存在的类称为超类、基类或父类,新类称为子类、派生类或孩子类
继承可以增加字段、增加方法或覆盖方法,但绝对不会删除任何字段或方法
所有的继承都是公共继承
子类不能直接访问父类的私有字段
子类比超类拥有的功能更多
一般将通用功能抽取到超类,而将更特殊的方法放在子类中
覆盖方法
提供一个相同签名的新方法来覆盖超类的这个方法
使用super关键字访问被覆盖的超类方法
super.getSalary()
super与this的区别在于this是对本对象的引用,而super仅仅指示编译器调用超类方法
允许子类将覆盖方法的返回类型改为原返回类型的子类型,称这两个方法有可协变的返回类型
覆盖一个方法时,访问修饰符要保证子类方法不能低于超类方法的可见性
子类构造器
子类的构造器不能访问超类的私有字段,因此只能通过调用超类的构造器来初始化这些私有字段
通过super语句调用超类构造器,super语句必须是子类构造器的第一条语句
pulic Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
bonus=0;
}
如果子类的构造器没有显示的调用超类的构造器,将自动调用超类的无参数构造器,如果没有就会报告一个错误
一个对象变量可以指示多种实际类型的现象称为多态,在运行时能动态的选择适当的方法,称为动态绑定
继承层次
继承层次指由一个公共超类派生出的所有类的集合
在继承层次中,从某个特定的类到其祖先的路径称为继承链
子类之间没有任何关系
Java不允许多重继承
多态
子类的每个对象也是超类的对象
对象没有子对象,只有指向其他对象的指针
出现超类对象的任何地方都可以使用子类对象替换,可以将子类对象赋给超类对象变量
对象变量是多态的,一个父类型的对象变量既可以引用超类的对象,也可以引用子类的对象
即使一个超类对象变量引用子类的对象,但编译器只将它看做是超类的对象
使用多态,而不要使用类型信息,比使用多个类型检测的代码更易于维护和扩展
对于需要使用多态的代码,可以定义一个方法,将其放置在两个类型的超类或接口中
理解方法的调用
-
编译器查看对象的声明类型和方法名
-
确定参数类型,进行重载解析
-
如果是private方法、static方法、final方法或者构造器,则进行静态绑定
如果是public方法、protect方法、包可见方法等可能依赖于隐式参数的实际类型,则在运行时动态绑定
-
然后程序运行并采用绑定调用方法时,查询虚拟机预先为每个类创建的方法表,其中列出了所有方法的签名和要调用的实际方法
动态绑定的重要特性是无需对现有代码进行修改就可以对程序进行修改
阻止继承
final类不允许子类扩展,将一个类声明为final,则其中的方法自动地成为final,但不包括字段
final方法不能被覆盖,如果一个方法不能被覆盖而且很短,编译器就能对它进行内联优化
可确保不会在子类中改变语义,可避免动态绑定带来的系统开销
即时编译器能准确直到类之间的关系,并检测方法覆盖,然后进行内联优化,直到虚拟机加载了另外一个子类覆盖了这个内联方法
强制类型转换
只能在继承层次内进行强制类型转换
在将超类转换为子类之前,使用instanceof操作符进行检查
只有需要使用子类方法时才需要强制类型转换
如果使用强制类型转换,则超类的设计上可能存在问题
抽象类
祖先类更具有一般性,只将它作为派生其他类的基类,而不是用来构造实例
使用abstract关键字,使一个方法被声明为抽象方法而不需要被实现
包含一个或多个抽象方法的类自身必须被abstract关键字声明为抽象类
public abstract class Person
{
...
public abstract String getDescription();
}
抽象类可以包含字段和具体方法
如果子类中保留了抽象类的部分或所有抽象方法仍未定义,那么子类也是抽象方法
不含抽象方法也可以将类声明为抽象类
抽象类不能实例化,但可以声明一个抽象类的对象变量,引用非抽象的子类对象
受保护访问
使用protected将一个方法或字段声明为受保护的,对本包和本包中所有子类可见
并不能带来过多的保护,有破坏封装性的隐患
对于指示那些不提供一般用途而应在子类中重新定义的方法很有用
修饰词 | 本类 | 同一个包的类 | 子类 | 其他类 |
---|---|---|---|---|
private | √ | × | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
无(默认) | √ | √ | × | × |
Object:所有类的超类
如果没有明确指出超类,Object就被认为是这个类的超类
Object类型的变量
可以使用Object变量引用任何类型的对象,只能作为各种值的泛型容器
要想进行具体操作,需进行相应的强制类型转换
所有引用类型和数组类型都拓展了Object类,只有基本类型不是对象
可以将基本类型的数组转换为Object,但不能将其转换为Object[ ]
equals方法
equals方法用于检测一个对象是否等于另一个对象
Object类中实现的equals方法将确定两个对象的引用是否相等
可在类中覆盖Object类的equals方法
在子类中定义equals方法时,首先调用超类的equals,如果超类的字段都相等,就需要比较子类的字段
相等性测试与继承
java语言规范要求equals方法有一下特性:
-
自反性:x.equals(x)为true
-
对称性:x.equals(y)与y.equals(x)有相同的结果
如果子类有自己的相等性概念,则对称性需求将强制使用getClass检测
如果由超类决定相等性概念,则可以使用instanceof检测,以在不同子类的对象之间比较
-
传递性:x.equals(y)为true且y.equals(z)为true,则x.equals(z)为true
-
一致性:如果x,y引用的对象没有发生改变,则结果不会改变
-
null值作为参数则返回false
hashCode方法
散列码是由对象导出的一个整型值
Object类中实现的hashCode方法将从对象的存储地址得出一个散列码
String类中实现的hashCode方法将从字符串的内容得出一个散列码,而StringBuilder沿用Object类中实现的hashCode方法
如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法
两个相等的对象返回相等的散列码
toString方法
返回表示对象值的一个字符串
在标准类库中,许多类都定义了toString方法,以便让用户获得一些有关对象状态的信息
只要对象和字符串通过+连接起来,编译器就会自动调用toString方法
数组继承了Object类中的toString方法,但可以调用静态Arrays.toString方法来返回以各个元素值表示的字符串,对于多维数组则调用静态Arrays.DeepToString方法
泛型数组列表
ArrayList类是一个有类型参数的泛型类,使用<>接收类型参数
在添加和删除元素时能自动的调整数组容量
声明数组列表
ArrayList<Employee> staff=new ArrayList<Employee>();
var staff=new ArrayList<Employee>();
ArrayList<Employee> staff=new ArrayList<>();
数组列表管理者一个内部的对象引用数组,当数组空间用尽,将自动创建一个更大的数组,并将原数组拷贝
可以使用ensureCapacity方法在填充数组之前分配一个指定大小的内部数组
也可以将初始容量传递给构造器
size方法返回包含的实际元素个数
一旦确定数组大小将保持恒定,调用trimToSize方法将存储块的大小调整为保存当前元素数量所需的存储空间
访问数组列表元素
使用get方法得到一个数组列表元素
使用add方法添加或插入元素
使用set方法设置某个元素
使用toArray方法将数组元素拷贝到数组列表
使用remove方法删除一个元素
插入和删除元素效率很低
类型化与原始数组列表的兼容性
与没有使用类型参数的遗留代码交互操作
可以直接将类型化ArrayList传递给原始ArrayList参数
将原始ArrayList传递给类型化ArrayList参数会得到一个警告,使用强制类型转换也不能避免
在虚拟机中没有类型参数,所有类型化ArrayList都将转换为原始ArrayList,所有的数组列表都是一样的
对象包装器与自动装箱
所有基本类型都有与之对应的类,称这些类为包装器。Integer、Long、Short、Byte、Float、Double、Character、Boolean
通过包装器将基本类型转换为对象
包装器是不可变的,不能更改其中的值,也不能派生子类
类型参数不能是基本类型,只能使用包装器作为类型参数
由于每个值都要进行包装,ArrayList< Integer>效率远低于int[ ]数组
当需要包装器类型的参数而传递了一个基本类型值时,将自动使用静态Integer.valueOf方法,转换为包装器类型,称为自动装箱
当将一个包装器对象赋给一个基本类型变量时,将自动调用intValue方法,转换为基本类型,称为自动拆箱
包装器对象应用=运算符,以检测对象是否有相同的位置。使用equals方法比较值
包装器类引用可以为null,但自动装箱时会抛出异常
如果拆箱后的基本类型在表达式中进行了类型转换,则将装箱为对应的类型
Java设计者将一些静态的方法放在包装器中,可以方便的操作基本类型值
编写一个可以修改参数值的方法,可以使用org.omg.CORBA包中包含的持有者类,IntHolder、BooleanHolder等,拥有公共字段value用于访问并修改
参数数量可变的方法
称为变参方法
使用…表示可以接收任意数量的对象,相当于数组
public PrintStream printf(String fmt,Object... args) //Object...等同于Object[]
{
return format(fmt,args);
}
public static void main(String... args){}
可以将数组传递给可变参数
枚举类
使用enum关键字定义枚举类型
public enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE}
这个声明定义的是一个类,每一个枚举类型成员都是Size类中被 public static final 修饰的静态常量字段,当使用枚举类型成员时,直接使用枚举名称调用成员即可。
比较两个枚举值时,可直接使用==
可为枚举类型增加构造器、方法和字段,或者添加接口
public enum Size
{
SMALL,MEDIUM,LARGE,EXTRA_LARGE;
private String abbreviation;
private Size(String abbreviation){this.abbreviation=abbreviation;}
public String getAbbreviation(){return abbreviation;}
}
枚举的构造器总是私有的
所有的枚举类型都是Enum类的子类,并继承了toString方法、静态valueOf方法、静态values方法、ordinal方法
String s=Size.SMALL.toString(); //返回字符串“SMALL”
Size size=Enum.valueOf(Size.class,"SMALL");//将普通字符串转换为枚举实例
Size[] values=Size.values(); //返回枚举类型的所有成员
int m=Size.MEDIUM.ordinal(); //返回枚举常量的位置
反射
使用反射库动态操纵Java代码的程序
能够分析类能力的程序称为反射,主要用于开发工具
反射很脆弱,编译器将无法查找编程错误
Class类
程序运行时,Java运行时系统始终为所有对象维护一个运行时类型标识(类型指类和基本类型)
Class类型保存这些信息,Class类实际上是一个泛型类
Class对象用于描述一个特定类型的属性,虚拟机为每个类型管理一个唯一的Class对象
使用==实现两个类对象的比较,区别于instanceof
可以调用Object类中的getClass方法返回一个Class类的实例
可以使用Class类中的静态forName方法接收一个保存类型名的字符串,获得类型名对应的Class对象,还可以用于强制加载类
可以使用T.Class代表T类型的类对象
类对象通过调用getConstructor方法得到Constructor类型的对象
Constructor类型的对象通过调用newInstance方法构造一个实例,并把所有构造器异常包装到InvocationTargetException
资源
类关联的数据文件称为资源
将资源与其他程序文件一起放在JAR文件中
虚拟机知道如何查找一个类,所以它能搜索与类文件放在同一个目录中的资源文件,也可以提供一个相对或绝对路径
文件的自动装载是利用资源加载特性完成的
每个程序必须有自己的方法来解释资源文件
如果有接受URL的加载资源的方法,可以通过类对象调用getResource方法得到这个类存放某个资源的地址
如果没有,则通过类对象调用getResourceAsStream方法得到一个输入流来读取文件中的数据
利用反射分析类的能力
Field、Method、Constructor三个类用于描述类的字段、方法、构造器
都有一个getName方法返回字段、方法、构造器的名称
都有一个getModifiers方法返回用于描述修饰符的整数值,或者使用静态Modifier.getModifiers方法,使用isPublic、isPrivate、isFinal方法判断修饰符,使用静态Modifier.toString方法将修饰符打印出来
Field类的getType方法返回字段类型的类对象
getFields、getMethods、getConstructors方法返回这个类支持的公共字段、方法、构造器的数组
getDeclareFields、getDeclareMethods、getDeclareConstructors方法返回这个类声明的全部公共字段、方法、构造器的数组
使用反射在运行时分析对象
获得相应的类对象,通过类对象调用getDeclaredField方法或getField方法得到对应字段名的一个Field对象
var harry=new Employee("Harry Hacker",5000,10,1,1989);
Class cl=harry.getClass();
Field f=cl.getDeclaredField("name");
反射机制的默认行为受限于访问机制
可以调用Field、Method、Constructor对象的setAccessible方法覆盖Java访问控制
f.setAccessible(true);
访问可被模块系统、安全管理器拒绝,然后抛出异常
Field对象调用get方法返回一个值为当前字段值的对象,调用示set方法将字段设置为新值
Object v=f.get(harry);
f.set(harry,"Hacker");
将来的库使用可变句柄而不是反射来读写字段
可以覆盖toString方法,在toString方法中获得数据字段,并设置访问,然后递归的调用toString方法,将每个值转换为字符串
使用反射编写泛型数组代码
使用静态Array.newInstance方法构造一个新数组
使用Class类的getComponentType方法确定数组类型
Class ComponentType=cl.getComponentType;
Object newArray=Array.newInstance(ComponentType,newLength);
调用任意方法和构造器
没有途径将一个方法的储存地址传给另一个方法,但使用接口和lambda表达式是更好的解决方案
使用反射机制调用任何方法
通过Method对象调用invoke方法,以调用包装在当前Method对象内的方法
如果方法的返回类型是基本类型,invoke将返回其包装器类型
可调用getDeclareMethods方法返回Method对象数组,然后搜索想要的方法
也可通过类对象调用getMethod方法得到对应签名的一个Method对象
Method m=Employee.Class.getMethod("raiseSalary",double.Class);
invoke的参数和返回值必须是Object类型,多次的强制类型转换使丧失检查代码的机会
使用反射得到方法指针比直接调用方法慢得多