-
- 1.1. 关于变量的访问顺序
-
- 4.1. 阻止继承:final类和方法
-
- 8.1. equals方法
- 8.2. hashCode方法
- 8.3. toString方法
-
- 9.1. 访问数组列表的元素
- 9.2. 对象包装器和自动装箱
-
- 12.1. 获取Class类的对象
- 12.2. 反射获取构造方法并使用
- 12.3. 反射获取成员变量并使用
- 12.4. 反射获取成员方法并使用
- 12.5. 通过反射越过泛型检查
- 12.6. 反射总结
继承
如果两个对象存在“is-a”关系,即某某是某某,可以使用继承。
使用关键字extends表示继承(c++中是 : ),关键字extends表示正在构造的新类派生于一个已存在的类。这个已存在的类称之为基类,超类,父类;新类 称为子类、派生类
1. 覆盖方法
超类中的一些方法对子类不一定适用,需要提供一个新的方法覆盖超类中的方法。
- 现假设有一个Employee(雇员类)和Manager(经理类),其中Employee中有一个薪资getSalary()方法及加薪raiseSalary()方法,Manager有一个奖金setBonus()方法.
- Manager继承Employee
Employee类
1 | public class Employee { |
Manager类
1 | public class Manager extends Employee{ |
Manager类中的getSalary()方法应该返回薪资和奖金的总和.所以需要在Manager中提供一个新的方法来覆盖超类Employee中的方法
1 | public class Manager extends Employee{ |
- 刚开始一般都会这么想
1 | public double getSalary() { |
salary是Employee类的私有成员,只有他自己能够访问
- 那么可不可以调用Employee的访问器来访问salary呢?
1 | public double getSalary() { |
这是个死循环,覆盖的方法名和被覆盖的方法的名字是保持一致的!,它会首先在本类中寻找,因此这个方法会无限调用自己,应该使用关键字super来访问Employee中的getSalary方法
1 | public double getSalary() { |
super和this相对应,一个表示父类,一个表示本身
警告:在覆盖方法时,子类方法不能低于超类方法的可见性!
1.1. 关于变量的访问顺序
如果在子类方法中访问一个变量
- 现在子类局部变量找
- 子类成员范围找
- 父类成员范围找(非私有)
- 没找到就报错
方法中的局部变量会覆盖成员变量,虽然也可以使用super和this来访问成员变量,但是在子类方法中定义的局部变量要尽量不与成员变量同名!
2. 子类构造器
子类构造器一定要调用超类的构造器,如果子类构造器没有显式地调用超类的构造器,将自动调用超类的无参构造器
但是如果超类没有提供无参构造器,子类又有没有显式地调用超类其他构造器,java编译器会报错!
3. 多态
在java中,对象变量是多态的。
例如
1 | Employee e; |
即一个对象变量即可以引用其本身类型的变量,也可以引用任何一个子类的对象.
但是: 编译器只将e看做是Employee的对象,e只能访问Employee类的方法,不能访问子类特有的方法。
1 | //Manager类有个自己的方法setBonus |
注意: 子类不能引用父类
1 | Manager m = staff[1];//error; |
3.1. 多态的前提和体现
- 有继承/实现关系
- 有方法重写
- 有父类引用指向子类对象
3.2. 多态的好处与弊端
- 提高了程序的拓展性:定义方法的时候,使用父类型作为参数,将来使用的时候可以使用子类作为参数
- 不能使用子类特有的功能
4. 理解方法调用
编译器查看对象的声明类型和方法名
确定方法调用中提供的参数类型
静态绑定:如果是static、private、final方法或者构造器,编译器将准确地知道应该调用哪个方法
动态绑定:程序运行并且采用动态绑定(多态)调用方法时,虚拟机必须调用与x所引用对象的实际类型所对应的方法。假设x的实际类型为d,它是c的子类,如果d定义了这个方法就会调用这个方法;否则将在d类的超类中寻找。
4.1. 阻止继承:final类和方法法
将某个特定的方法声明为final,子类就不可以重写(覆盖)这个方法。
将某个类声明为final,其中所有的方法都会被声明为final,但不包括字段
5. 强制类型转换
当一个父类对象变量引用子类对象时,如果要访问子类独有的方法,则需要进行强制类型转换
1 | Employee[] staff = new Employee[3]; |
前提是父类对象变量的实际类型要与转换类型一致,如果不一致将会发生错误
1 | Manager boss = (Manager)staff[1];//error,staff[1]的实际类型时Employee |
因此在进行强制转换前,最好检查一下是否能成功地转换
1 | if(staff[1] instanceof Manager) |
综上所述:
- 只能在继承层次内进行强制类型转换
- 在将超类强制转换为子类之前,应该使用instanceof进行检查
6. 抽象类
可以用关键字abstract来声明一个类,说明这个类为抽象类。
1 | public abstract class animal { |
说明这个类不能被实例化,也就是不能创建抽象类的对象
1 | animal an = new animal();//error |
不过可以定义一个抽象类的对象变量,只能引用非抽象子类的对象
1 | public class cat extends animal {//非抽象子类 |
可以将方法声明为抽象方法,那么就不需要实现这个方法了
1 | public abstract class animal { |
抽象方法的功能由其子类实现,抽象类的子类要么重写抽象类中的抽象类,要么也定义为抽象类!
注意事项:
- 抽象方法必须在抽象类中,有抽象方法的类一定为抽象类,但抽象类可以没有抽象方法。
- 抽象类可以有构造器,主要用于子类访问父类数据的初始化。
- 抽象方法类似于c++中的纯虚函数。
- 将通用字段和方法放在超类中
7. 访问修饰符小结
修饰符 | 权限 |
---|---|
private | 仅对本类可见 |
public | 对外部完全可见 |
protected | 对本包和所有子类可见 |
默认,不需要修饰符 | 对本包可见 |
8. Object:所有类的超类
java中每个类都扩展了Object,如果没有明确指出超类,Object就被认为是这个类的超类。
可以使用Object类型的变量引用任何类型的对象
1 | Object obj = new Employee(); |
但如果相对其中的内容进行具体的操作,需要进行强制转换
1 | Employee e = (Employee)obj; |
8.1. equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象,但只是确定两个对象引用是否相等。
我们来看一下equals方法的源码
1 | public boolean equals(Object obj) { |
他依然是比较两个对象的地址值,只有当两个变量引用同一个对象时才会相等,这和“==”是等效的!
1 | animal a = new cat(); |
如果要比较两个对象的内容是否相等,需要对equals方法进行重写。
在子类定义equals方法时,首先调用父类的equals进行比较(因为通用字段在父类中)。如果超类字段都相等,再比较子类字段。
为了防备两边都为null的情况,有一种Objects.equals(a,b)方法。如果两边都为null,返回true;只有一个为null,返回false;都不为null,则调用a.equals(b).
java中对于equals方法的规则如下:
- 自反性:对于任何非空引用x,x.equals(x)应该返回true。
- 对称性:对于任何x和y,当且仅当y.equals(x)返回true时,x.equals(y)返回true.
- 传递性:对于任何引用x,y,z,如果x.equals(y)为true,y.equals(z)为true,那么x.equals(z)为true。
- 一致性:如果x和y的引用没有发生变化,反复调用x.equals(y)应该返回同样的值。
- 对于任何非空引用x,x.equals(null)应该返回false。
idea中可以用快捷键重写,重写equals时也会自动重写hashcode。
8.2. hashCode方法
x.hashCode()返回散列码,Object默认会从对象的存储地址得出散列码。
equals与hashCode的定义必须相容:如果x.equals(y)返回true,那么x.hashCode()与y.hashCode()返回相同的值.
8.3. toString方法
源码为
1 | public String toString() { |
idea中也可以用快捷键重写
如果x是一个任意对象,并调用System.out.println(x),println方法会简单地调用x.toString方法,并打印输出得到的字符串。
数组继承了Object类的toString方法,并没有覆盖
1 | int[]arr = new int[]{1,2,3}; |
因此要打印数组,要调用静态方法Arrays.toString
1 | System.out.println(Arrays.toString(arr)); |
9. 泛型数组列表ArrayList
ArrayList相当于c语言中的顺序表(Linklist相当于链表),它是一个泛型类。
声明和构造一个数组列表
1 | ArrayList<String> objects = new ArrayList<>();//右边的<>内容可以省略 |
尖括号内的类型参数不允许是基本类型! 要使用包装器。
使用add方法将元素添加到数组列表当中
1 | objects.add("hello"); |
如果调用add而内部数组已满,数组列表就会自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
如果已经知道或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法
1 | objects.ensureCapacity(100);//分配一个包含100个对象的内部数组 |
这样一来,前100次add调用不会带来开销很大的重新分配空间,或者还可以这样
1 | ArrayList<String> objects = new ArrayList<>(100);//直接传给构造器 |
数组列表的容量和数组的大小是有区别的,容量只是可能保存的元素的个数;而数组的大小就说明有这么多个空位置可以使用。
9.1. 访问数组列表的元素
不能像数组一样用[ ]来访问元素,而是用get来访问数组列表的元素。
1 | ArrayList<String> objects = new ArrayList<>(3); |
如果要修改某个元素的值,则要使用set方法
1 | objects.set(2,"yeah!"); |
数组列表的容量和实际大小是不同的
1 | ArrayList<String> objects = new ArrayList<>(5); |
还可以删除数组列表中的某个元素
1 | String str = objects.remove(2);//返回被删除的元素 |
for each循环也同样适用于数组列表
1 | for(String s:objects){ |
9.2. 对象包装器和自动装箱
所有基本类型都有一个与之对应的类,这些类称之为包装器。
|基本类型| 包装器 |
|–|–|
|int |Integer |
|long|Long|
|float|Float|
|short|Short|
|byte|Byte|
|char|Character|
|boolean|Boolean|
同时包装器类还是final,不能派生他们的子类。
对于那些泛型类包容的是对象类型,即泛型类的类型参数要是对象。而基本类型不是对象,可以用包装器代替基本类型。
1 | ArrayList<int> list = new ArrayList<>();//error |
有一个很方便的特性能将基本数据类型传进数组列表
1 | list.add(3);//ok,等价于以下形式 |
自动装箱与拆箱也适用于算术表达式
1 | Integer n = 3;//ok,装箱 |
包装器和基本类型有一点很大的不同,基本类型可以用”==”进行比较,而包装器是引用类型,要用equals方法进行比较。
最后强调一下,装箱和拆箱是编译器要做的工作,而不是虚拟机。
如果想要构造一个Integer对象,则需要使用工厂方法
1 | Integer integer = Integer.valueOf(String s);//s为数字字符串 |
10. 参数数量可变的方法
可以提供参数数量可变的方法,用…即可
1 | public static void meth(int...num){ |
括号中的参数都为int类型,参数的个数是可变的,调用以上方法
1 | meth(1,2,3,4,5,6); |
可见,参数int…num实际为数组,可以看成int[ ]num,它将所有的参数保存在数组之中,具体事例可以参考printf方法的实现。
11. 枚举类
对于定义一个枚举类型
1 | public enum size{small,mid,large} |
这个声明定义的类型是一个类,它刚好有四个事例,不可能构造新的对象。
比较两个枚举类型的值可以直接使用==
我们也可以为其添加构造器,方法和字段
1 | public enum size{ |
枚举类的构造器总是私有的。
所有枚举类型都是Enum类的子类,其中继承过来的toString方法将返回枚举常量名。
1 | size s = size.small; |
每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
1 | size[] si = size.values(); |
compareTo可以用来比较两个枚举常量的大小,实现略
12. 反射
java反射机制,是指在运行时去获取一个类的变量和方法信息,然后通过获取到的信息来创建对象,调用方法的一种机制。
12.1. 获取Class类的对象
要想通过反射去使用一个类,首先我们要获取该类的字节码文件对象,也就Class类型的对象。
这里有个测试类
1 | package reflection; |
有三种方法来获取该测试类的Class对象
1 | Class<test> testClass = test.class; |
Class类是一个泛型类,它表示每个类运行时的类型信息,每个类都有个Class类的对象。
虚拟机为每个类型管理一个唯一的Class对象,因此可以用==来实现两个类对象的比较
1 | test t = new test(); |
12.2. 反射获取构造方法并使用
如果有一个Class类型的对象,可以用它构造类的实例。调用getConstructor方法将得到一个Constructor类型的对象,然后使用newInstance方法来构造一个实例。
1 | //获取class对象 |
其中getConstructor方法中的参数为Class类型的对象,指定要调用的构造器,并且只能是非私有的构造器,如果需要获取所有类型的构造器,需要用getDeclaredConstructor方法
newInstance方法返回一个Object类的引用
12.3. 反射获取成员变量并使用
可以通过getField方法来获取成员变量,由于许多成员变量都是私有的,因此要使用getDeclaredField方法来获取私有变量,返回一个Filed对象
1 | Field age = c.getDeclaredField("age");//访问变量名为age的私有变量 |
获取了变量之后,便可以对其进行操作,但是默认是不能对私有成员进行操作的! 因此需要调用setAccessible方法来取消检查
1 | age.setAccessible(true); |
现在可以用set方法来修改成员变量,第一个参数为所要修改的对象
1 | age.set(o,5);//修改对象o中的age为5 |
get方法来返回这个Field对象所描述的对象的值
1 | System.out.println(age.get(o));//5 |
下面来看一个例子,要求通过反射来修改类中的字段
1 | public class demo { |
12.4. 反射获取成员方法并使用
与获取成员变量一样,方法可以通过getMethod来获取,返回一个Method对象(这些也都是非私有的)
1 | Method getAge = c.getMethod("getAge");//第一个参数为方法名 |
通过invoke方法来使用调用的方法,如果返回类型是基本类型,invoke方法会返回其包装器类型,invoke方法的第一个参数为所调用方法的对象,第二个为传进方法的值
1 | Method getAge = c.getMethod("getAge"); |
下面来看例子,通过反射来调用方法并使用
1 | public class demo2 { |
12.5. 通过反射越过泛型检查
对于泛型类ArrayList,一旦尖括号中的类型定了,就不能传其他的值进去。
1 | ArrayList<Integer> arr = new ArrayList(); |
这时可以利用反射来越过泛型检查
1 | Class<? extends ArrayList> cl = arr.getClass(); |
12.6. 反射总结
利用反射来操作类的步骤大致为以下几点
- 构造类的Class对象(三种方法)。
- 通过Class对象来获取构造器来创建实例。
- 通过所构造的Class对象的getMethod和getField方法来获取类的方法和成员变量。
- 使用返回的Field类所提供的各种方法来操作成员变量。
- 使用返回的Method类的invoke方法来使用所调用的方法。
- 如果要访问私有方法、构造器、变量,要使用setAccessible(true)。
注意事项: