第 5 章 继 承
继承(inheritance)是面向对象程序设计中的一个基本概念,是多态的基础,正是有了继承,才有了多态的特性。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。也就是说,继承已存在的类其实就是复用(继承)这些类的方法和域。当然,在复用的基础上我们还可以根据需求增加新的域和方法,以此来对子类进行扩展。
Java 中继承要点总结:
1. 父类的构造方法不能被继承,但可以在子类中通过 super 关键字访问。
2. 子类只能继承父类的非私有属性和方法,但是可以通过 Get 和 Set 方法访问和修改父类的私有属性。
3. 如果子类有和父类同名的属性和方法则会将父类中的覆盖,此时可以使用 super 关键字来指明使用父类中的属性和方法。
4. 子类的每一个构造方法的第一条语句默认都是 super(),我们要保证子类的构造方法中的第一条语句是 super()。因为子类并不能访问超类的私有域,我们需要使用 super() 对超类的私有域进行初始化。
5. 如果子类的构造器没有显式地调用超类的构造器, 系统将自动地调用超类默认(没有参数 )的构造器。 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,Java 编译器将报告错误。
6. java在创建一个子类对象的时候,首先去调用父类的不带参数的构造方法,生成父类对象,然后再去调用子类的构造方法,生成子类对象。
7. 在子类中可以增加域、 增加方法或覆盖超类的方法,但是并不能删除继承的任何域和方法。
有一个用来判断是否应该设计为继承关系的简单规则, 就是“ is-a” 规则, 它表明子类的每个对象也是超类的对象,反之则不然。例如,每个经理都是雇员, 而并不是每一名雇员都是经理。因此, 可以将 Manager 类设计为 Employee 类的子类。
is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。
需要注意的是: Java 不支持多继承,但支持多重继承。而接口实现(关键字是 implements)又变相的使 Java 具有多继承的特性。
由于多继承会带来二义性,在实际应用中应尽量使用单继承。
Java 与 C++ 定义继承类的方式十分相似。不同的是Java 用关键字 extends 代替了 C++中的冒号(:),而且在 Java 中,所有的继承都是公有继承,并没有 C++ 中的私有继承和保护继承 。
5.1 类、超类和子类
在 Java 中使用 extends 关键字来表示继承,示例代码如下:
//A.java 被 B 继承,是B的超类。
public class A{ . . . }
//B.java 继承类 A,是类A的子类。
public class B extends A{ . . . }
关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类(上述示例代码中的 A)称为超类( superclass )、 基类(base class ) 或父类(parent class); 新类(上述示例代码中的 B)称为子类(subclass) 、派生类( derived class ) 或孩子类(child class )。
相比于超类,子类有着更为丰富的功能,因为子类在继承超类的基础上又进行了扩展,新增了具有自己特色的功能。
5.1.2 覆盖方法(重写)
对于子类来说,并不是超类的所有方法都适用,此时我们就需要提供新的方法来覆盖超类中不适用的方法。这种对超类方法实现过程进行的重新编写就叫做重写。
强制性异常:在编写程序的过程中必需抛出的异常,也就是编译期异常。
非强制性异常:与强制性异常相反,也就是运行期异常。有关强制性异常和非强制性异常的更多内容请参看Java零碎知识点整理中的第 7 章内容。
有关重写和重载的更多内容请参看Java零碎知识点整理中的第 8 章内容。
5.1.3 子类构造器(this和super)
super 和 this:我们并不能将 super 和 this 当成类似的概念,因为 super 不像 this一样是一个对象的引用, 它只是一个指示编译器调用超类方法的特殊关键字,我们不能将 super 赋给另一个对象变量。
关键字 this 有两个用途:一是引用隐式参数,二是调用该类其他的构造器,同样,super 关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候, 这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现(super(param1,param2);
)。
5.1.4 继承层次
继承层次:由一个公共超类派生出来的所有类的集合被称为继承层次。
继承链:在继承层次中, 从某个特定的类到其祖先的路径被称为该类的继承链。
5.1.5 多态
多态是同一个行为具有多个不同表现形式或形态的能力,如同一个接口,使用不同的实例而执行不同操作。
多态存在的三个必要条件:
1. 继承:存在继承关系。
2. 重写:子类重写父类的方法。
3. 父类引用指向子类对象:父类引用子类对象。
多态的实现方式:
1. 重写
2. 接口
3. 抽象类和抽象方法
5.1.6 理解方法调用
如有以下方法调用语句:
C c = new C();
c.f("hello");
调用过程描述如下:
1. 编译器査看对象的声明类型和方法名:编译器将会列举 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。
2. 编译器将査看调用方法时提供的参数类型。这个过程被称为重载解析,编译器会在所有名为 f 的方法中选择一个与调用语句提供的参数类型完全匹配的方法(若无法找到将会报错)。
3. 如果是 private 方法、 static 方法、 final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定(static binding )。 与此对应的是,调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。在我们列举的示例中, 编译器采用动态绑定的方式生成一条调用 f(String) 的指令。
4. 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与隐式参数所引用对象的实际类型最合适的那个类的方法。假设隐式参数的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String) 就直接调用它;否则, 将在 D 类的超类中寻找 f(String) 以此类推。
如果每次调用方法都要进行搜索,时间开销相当大。因此, 虚拟机预先为每个类创建了一个方法表(method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候, 虚拟机仅查找这个表就行了。如果调用 super.f(param), 编译器将对隐式参数超类的方法表进行搜索。
5.1.8 强制类型转换
如果我们试图在继承链上进行向下的类型转换,当运行这个程序时, Java 运行时系统将报告这个错误, 并产生一个 ClassCastException异常。为避免这一问题,我们应该在进行类型转换之前, 先查看一下是否能够成功地转换。这个过程简单地使用 instanceof 操作符就可以实现。
if(clazz instanceof Class){
Class a = (Class)clazz;
}
注意:在一般情况下,应该尽量少用类型转换和 instanceof 运算符。
instanceof 严格来说是 Java 中的一个双目运算符,左边是对象,右边是类。当对象是右边类或子类所创建对象时返回true,否则返回false。
更多有关instanceof运算符的内容请参看博客:https://www.cnblogs.com/ysocean/p/8486500.html
5.1.9 抽象类
抽象方法: 使用关键字 abstract 修饰,没有方法体的方法。
抽象类: 使用关键字 abstract 修饰,拥有抽象方法的类。
public abstract class Employee{}
抽象类的特点:
1. 抽象类不能被实例化,如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
3. 抽象类中的抽象方法只是声明,不包含方法体。
4. 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
我们可以定义一个抽象类的对象变量,但由于抽象类不能被实例化,该对象变量只能引用其具体子类对象,永远不会引用该抽象类对象。因此当我们使用抽象类的对象变量调用方法时,实际上调用的是其具体子类的方法。
5.2 Object: 所有类的超类
Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。一个类如果没有被明确的指出超类,Object 就被认为是这个类的超类。由于在 Java 中,每个类都是由 Object 类拓展而来的,所以熟悉这个类提供的所有服务十分重要。下面将对 Object 提供的部分服务进行介绍。
1. equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另一个对象,其判断依据是两个对象是否具有相同的引用,也就是对两个对象的地址进行比较。
在 Object 的派生类中,如果我们想使用 equals 方法判断两个对象的内容是否相同则需要对 equals 方法进行重写。而由于 equals 底层依赖于 hashCode 方法,我们想要使重写的 equals 方法生效还需要对 hashCode 方法进行重写。
基本数据类型的封装类以及 String 的 equals 方法是被重写过的,可以对两个对象的内容进行比较。
对于数组类型的域, 可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。
Object 类中的 equals 方法源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
有关 Java 中 equals,hashcode 和 == 的区别请参看:https://www.cnblogs.com/kexianting/p/8508207.html
Java 语言规范要求 equals 方法具有下面的特性:
1. 自反性: 对于任何非空引用 x, x.equals(x) 应该返回 true。
2. 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true, x.equals(y) 也应该返回 true。一定要注意对称性,不只 equals 方法,类似的方法都要考虑其对称性。
3. 传递性: 对于任何引用 x、 y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也应该返回 true。
4. 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
5. 对于任意非空引用 x, x.equals(null) 应该返回 false。
2. hashCode 方法
hash code:散列码,是由对象导出的一个整型值。
由于 hashCode 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。
String 中重新定义了 hashCode 方法,其散列码是根据内容导出的,因此内容相同的两个 String 的散列码是一致的。
3. toString 方法
用于返回表示对象值的字符串,Object 中的 toString 方法返回的是以十六进制表示的对象散列码字符串,源码如下:
public String toString() {
return getClass().getName() + "@" +Integer.toHexString(hashCode());
}
在 Java中,绝大多数(但不是全部)的 toString 方法的返回值都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
数组继承了 object 类的 toString 方法, 但其返回值仍旧遵循 Object 中的 toString 方法返回值格式。如[I@la46e30
,前缀 [I
表明是一个整型数组。我们可以调用静态方法 Arrays.toString 得到一个以类名开头,随后是一对方括号括起来的域值格式的返回值。
5.4 对象包装器与自动装箱
所有的基本类型都有一个与之对应的类,这些类称为包装器,比如 Integer 类是 基本数据类型 int 的包装器。
装箱:将基本数据类型转换成包装器类的过程。
拆箱:将包装器类转换为基本数据类型的过程。
Java 提供了自动拆\装箱功能。
包装器的超类 | 基本数据类型 | 包装器 |
---|---|---|
Number | int | Integer |
long | Long | |
float | Float | |
double | Double | |
short | Short | |
byte | Byte | |
- | char | Character |
- | void | Void |
- | boolean | Boolean |
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。
由于ArrayList<lnteger>
的每个值分别包装在对象中,其效率远远低于int[ ]
数组。 因此, 应该用ArrayList<lnteger>
构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
由于包装器类引用可以为 null, 所以自动装箱有可能会抛出一个 NullPointerException 异常,如下:Integer n = null; System.out.println(2 * n); // 会抛出一个空指针异常。
如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double, 再装箱为 Double,如:
Integer n = 1; Double x = 2.0; System.out.println(true ? n : x);输出结果为:1.0
装箱和拆箱是编译器认可的, 而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。
5.5 参数数量可变的方法
当我们在 Java 中声明一个方法时可以使用参数类型加...
的形式来表示可变参数,示例代码如下:
//Object...obj表示这是一个Object类型的可变参数
public void methodOne(String str, Object...obj){ . . . }
//调用:methodOne("str", obj1,obj2,obj3); 还可以有obj4,obj5等等
本质上Object...
和Object []
是一样的,完全可以把可变参数当成数组使用。通过反编译 class 字节码文件我们可以发现,可变参数被解释为了数组,这样就可以让编译器帮开发人员完成部分工作,进而减轻开发的部分工作量。
//和上述有可变参数的方法在本质上是一样的
public void methodOne(String str, Object [] obj) { . . . }
由于可变参数本质上是数组,因此我们可以将一个数组传递给可变参数。
methodOne("str", new Object[] {"obj1","obj2","obj3"});
注意:
1. 每个方法最多只能有一个可变参数,且可变参数必须是方法的最后一个参数。
2. 可变参数列表的方法的重载不同于普通方法,无法仅通过改变可变参数的类型,来重载方法。//如果有如下两个方法定义 public void methodOne(String str, Object ... obj) { . . . } public void methodOne(String str, int ... obj) { . . . } //如果有如下调用语句,则编译器会报错,提示方法定义不明确,存在歧义 methodOne("str", 11,11);
3. 可变参数方法的调用都会引起 array 的内存分配和初始化,这会给性能带来损耗,因此不建议入参个数少于四个的方法使用可变参数。
4. 由于可变参数的本质是数组,所有使用可变参数前一定要对其进行判空操作,并且要判断其长度是否正确。
5.6 枚举类
枚举类是一个使用enum
声明定义的特殊数据结构,其本质是一个继承了java.lang.Enum
的类。枚举类只能用来表示一个具有有限实例的类,我们称这有限的实例为枚举类的常量。如一周有七天,我们就可以用枚举类来表示周,其定义如下:
public enum DayOnWeek {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}
上面声明的枚举类有七个常量,分别表示一周的周一到周日。这七个常量就是七个实例,其也是通过调用构造器进行构造的。在我们没有给出自定义构造器的前提下,枚举类实例的构造使用的是父类 java.lang.Enum 的构造器。Enum 源码如下:
package java.lang;
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
//此枚举常量的名称
private final String name;
public final String name() {
return name;
}
//此枚举常数的序数,初始常数的序数为零,如上面的 DayOnWeek 中 MONDAY 的序数就为零。
private final int ordinal;
public final int ordinal() {
return ordinal;
}
//唯一的构造函数,该构造函数只能由编译器进行调用,我们不能主动调用。
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final boolean equals(Object other) {
return this==other;
}
public final int hashCode() {
return super.hashCode();
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
//比较的是ordinal值
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
}
枚举类和普通类有以下几个不同点:
1、枚举类不能指定继承的父类(因为继承了 java.lang.Enum 类),但是可以实现多个接口,枚举类默认实现了 Comparable 接口和Serializable接口。
2. 枚举类的构造方法的访问权限只可为 private 。
3. 枚举类的实例必须显式列出。
鉴于简化的考虑, Enum 类省略了一个类型参数,例如, 实际上, 应该将枚举类型 Size 扩展为 Enum<Size>。
枚举类本质上也是一个类,也有着构造方法,我们完全可以为枚举类自定义构造方法,唯一需要注意的就是枚举类的构造方法必须是私有的。
更多内容可以参考:
https://www.cnblogs.com/alter888/p/9163612.html
https://blog.csdn.net/qq_27093465/article/details/52180865
5.7 反射(reflective)
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有方法和私有属性),这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。
JAVA 反射机制主要指应用程序访问、检测、修改自身状态与行为的能力。
5.7.1 Class类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。为了能够方便的使用这些类型信息,Java 使用 Class 类存储该信息,因此 Class 类的实例就表示正在运行的 Java 应用程序中的类和接口。
如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。
我们可以从JDK API 中获取以下几点信息:
1. Class 类的实例对象表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有很多的实例,每个类都有唯一的Class对象。
2. Class 类没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机自动构造的。也就是说我们不需要创建,JVM已经帮我们创建了。
3. Class 对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法。
4. 一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类,例如,int 不是类, 但 int.class 是一个 Class 类型的对象。基本数据类型(int、char、boolean等)和关键字 void 也表示为 Class 对象。
- 与 Class 类相关的常用方法介绍
所属类 | java.lang.Object | |
---|---|---|
方法 | public final Class<?> getClass() | |
描述 | 返回调用该方法对象的运行时类。返回的类对象是被所表示类的静态同步方法锁定的对象。 | |
所属类 | java.lang.Class<T> | |
方法 | public String getName() | |
描述 | 以字符串的形式返回这个类对象表示的实体(类、接口、数组类、基本类型或void)的名称(全类名)。 | |
方法 | public static Class<?> forName(String className) throws ClassNotFoundException | |
描述 | 返回与给定字符串名的类或接口关联的类对象。入参是以字符串表示的目标类对象全类名。 String className = "java.util .Random"; Class cl = Class.forName(className) ; | |
方法 | public T newInstance() throws InstantiationException, IllegalAccessException | |
描述 | 创建一个由 Class 类对象表示的类的新实例,该方法调用默认的无参构造器,可以用来动态地创建一个类的实例。 String s = "java.util .Random"; Object m = Class.forName(s).newInstance(); |
获取 Class 对象的三种方式:
5.7.3 利用反射分析类的能力
在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、方法和构造器。
上述三个类都有叫做 getName 的方法和叫做 getModifiers 的方法。其中 getName 方法用来返回属性、方法、构造器的名称; getModifiers 方法将返回一个用于描述 public 和 static 这样的修饰符使用状况的整型数值,我们可以利用 java.lang.reflect 包中的 Modifier 类的静态方法分析该整型数值。例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal 判断方法或构造器是否是 public、private 或 final,还可以利用 Modifier.toString 方法将修饰符打印出来。
Class 类中的 getFields、 getMethods 和 getConstructors 方法将分别返回类公有的域、方法和构造器数组(包括从超类继承来的域即方法)。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。
示例代码如下:
父类 ParentClass
public class ParentClass {
public String parPubField;
protected String parProField;
String parDefField;
private String parPriField;
public void parPublicMethod() {
System.out.println("这是父类的公共方法。。。");
}
private void parPrivateMethod() {
System.out.println("这是父类的私有方法。。。");
}
}
子类 ChildClass
public class ChildClass extends ParentClass {
public String chiPubField;
protected String chiProField;
String chiDefField;
private String chiPriField;
public void chiPublicMethod() {
System.out.println("这是子类的公共方法。。。");
}
private void chiPrivateMethod() {
System.out.println("这是子类的私有方法。。。");
}
}
测试主方法:
public class ReflectionTest {
public static void main(String[] args) {
try {
// 第一种获取 Class 的方式
Class clazz1 = Class.forName("com.sk.reflective.ChildClass");
// 第二种获取Class的方式
Class clazz2 = ChildClass.class;
// 第三种获取Class的方式
ChildClass childClass = new ChildClass();
Class clazz3 = childClass.getClass();
System.out.println("获取公共的域、方法、构造器:");
System.out.println(" 公共域(包括从父类继承的)有:");
Field[] fields = clazz1.getFields();
for (Field field : fields) {
System.out.print(" " + field.getName());
}
System.out.println();
System.out.println(" 公共方法(包括从父类继承的)有:");
Method[] methods = clazz1.getMethods();
for (Method method : methods) {
System.out.print(" " + method.getName());
}
System.out.println();
System.out.println(" 公共构造器有:");
Constructor[] constructors = clazz1.getConstructors();
for (Constructor constructor : constructors) {
System.out.print(" " + constructor.getName());
}
System.out.println();
System.out.println("获取类所有的域、方法、构造器:");
System.out.println(" 全部域(不包括从父类继承的)有:");
Field[] declaredFields = clazz1.getDeclaredFields();
for (Field field : declaredFields) {
System.out.print(" " + field.getName());
}
System.out.println();
System.out.println(" 全部方法(不包括从父类继承的)有:");
Method[] declaredMethods = clazz1.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.print(" " + method.getName());
}
System.out.println();
System.out.println(" 全部构造器有:");
Constructor[] declaredConstructors = clazz1.getDeclaredConstructors();
for (Constructor constructor : declaredConstructors) {
System.out.print(" " + constructor.getName());
}
System.out.println("getModifiers方法的使用:");
Field field = fields[0];
System.out.println(" 使用的域的名称:" + field.getName());
int modifiers = field.getModifiers();
System.out.println(" getModifiers方法获取到的值:" + modifiers);
System.out.println(" 通过modifiers值判断目标域是否为公有的:" + Modifier.isPublic(modifiers));
System.out.println(" 使用Modifier.toString转换modifiers:" + Modifier.toString(modifiers));
System.out.println("调用方法:");
Method method = methods[0];
System.out.print(" ");
//对于非静态方法,只有在实例化对象之后才能执行方法,所以这里的入参必须是实例化后的对象。
method.invoke(childClass);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
5.7.4 在运行时使用反射分析对象
我们要注意运行时,此时已经存在实例化的对象,而下面将要介绍的 get 方法的入参就必须是实例化后的对象。
我们可以通过 Field 类中的 get 方法进一步查看数据域的实际内容,对 get 方法的介绍如下:
所属类 | java.lang.reflect.Field | |
---|---|---|
方法 | public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException | |
描述 | 返回指定对象(实例化对象)上由该字段表示的字段的值。如果该值为基本类型数据,则将被自动装箱为相对应的对象。 需要注意的是入参必须为实例化后的对象,可以理解为该方法从这个实例化对象所在的存储空间内提取由 Field 表示的对应字段的值。 |
如果我们对一个对象的私有域数据直接使用 get 方法,get 方法会抛出一个
IllegalAccessException 异常,例如我们直接使用 get 方法获取上一节 ChildClass 类中的 chiPriField 域数据,get 方法就会抛出异常,代码如下 :
Field field2 = clazz1.getDeclaredField("chiPriField");
Object object2 = field2.get(childClass);
注意:get 方法的入参必须为实例化的对象。
抛出异常内容为:
java.lang.IllegalAccessException: Class com.sk.reflective.ReflectionTest can not access a member of class com.sk.reflective.ChildClass with modifiers "private"
反射机制默认是不允许访问类的私有域、方法、构造器的,因此不只是抽取私有数据域会发生上述异常,直接利用反射操作私有方法和私有构造器都会发生上述异常。为避免这一情况,我们可以使用 Field、Method、Constructor 类的 setAccessible 方法为反射对象设置可访问标志。
所属类 | java.lang.reflect 包下的 Field、Method、Constructor 类 | |
---|---|---|
方法 | public void setAccessible(boolean flag) throws SecurityException | |
描述 | 为反射对象设置可访问标志。 flag 为 true 表明屏蔽 Java 语言的访问检查,使得对象的私有属性也可以被査询和设置。 |
示例代码:
Field field2 = clazz1.getDeclaredField("chiPriField");
field2.setAccessible(true);//设置可访问性
Object object2 = field2.get(childClass);
Method declaredMethod = clazz1.getDeclaredMethod("chiPrivateMethod");
declaredMethod.setAccessible(true);//设置可访问性
declaredMethod.invoke(childClass);//执行方法
setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和 Constructor 类的公共超类。
5.7.5 使用反射编写泛型数组代码
相关方法介绍:
所属类 | java.lang.System | |
---|---|---|
方法 | public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) | |
描述 | 从指定源数组中复制一个指定长度的数组到指定数组中,入参中的 src 为源数组,srcPos 为源数组中的起始复制位置,dest 为目标数组,destPos 为目标数组中放置复制数据的起始位置,length 为要复制的数组元素的数量。 | |
所属类 | java.lang.Class<T> | |
方法 | public boolean isArray() | |
描述 | 判定当前 Class 对象是否表示一个数组类。 | |
方法 | public Class<?> getComponentType() | |
描述 | 返回表示数组组件类型的 Class,即返回表示数组中存储的数据类型的 Class。如果当前 Class 类不表示数组类,则此方法返回 null,即该方法只能在对数组使用时才有实际意义。 | |
所属类 | java.lang.reflect.Array | |
方法 | public static Object get(Object array, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException public static Object getXxx(Object array, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException | |
描述 | 返回存储在给定位置上的给定数组的内容。入参分别为目标数组和目标索引。 Xxx 是 boolean、byte、char、double、float、int、long、short 之中的一种基本类型。 |
假如我们现有如下一个类:
public class Employee {
public String name;
public int age;
//省略 Get、Set 方法
}
下面将对书上给出的将 Employee[ ] 数组转换为 Object[ ] 数组的例子进行解析。
第一种方式:
public static Object[] badCopyOf(Object[] a, int newLength) {
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
return newArray;
}
这种方式返回的数组类型是 Object[ ] 类型,而且该返回数组从一开始就是一个 Object[ ] 类型。Java 能够记住创建数组时 new 表达式中使用的元素类型,我们将一个 Employee[ ] 临时地转换成 Object[ ] 数组, 然后再把它转换回来是可以的,但一从开始就是 Objectt ] 的数组却永远不能转换成 Employee[ ] 数组。
基于上述原因,下面一段代码执行时会抛出异常:
@Test
public void badCopyOfTest() {
Employee[] employees = new Employee[10];
Employee[] badCopyOf = (Employee[]) badCopyOf(employees, 10);
}
//异常信息为:java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Lcom.sk.consoletest.Employee;
为解决上述问题,我们需要能够创建与原数组类型相同的新数组,也就是将 badCopyOf 方法中声明的 Object[ ] 修改为与需要复制的数组类型相同的数组。
public static Object goodCopyOf(Object a, int newLength) {
//1.获取a数组的类对象
Class arrayClass = a.getClass();
//2.确认a是一个数组
if(!arrayClass.isArray()) return null;
//3.使用 Class 类的 getComponentType 方法确定数组对应的类型
Class componentType = arrayClass.getComponentType();
//4.获取a数组的长度
int length = Array.getLength(a);
//5.实例化新数组
Object newArray = Array.newInstance(componentType, newLength);
//6.复制数组
System.arraycopy(a, 0, newArray, 0, newLength);
return newArray;
}
关于上面的 goodCopyOf 方法,可能存在如下几点疑惑:
1. 入参为什么使用 Object 而不是 Object[ ] 类型?
这样是为了让该方法不仅能够扩展对象数组,还能够扩展任意类型的数组。整型数组类型 int[ ] 可以被转换成 Object ,但不能转换成对象数组。因此,我们使用 Object[ ] 类型作为入参时将不能扩展 int[ ] 等基本类型数组。
2. 数组 a 的长度为什么要用Array的 getLength 方法获取?数组本身不是有可以获取长度的方式吗?
我们需要注意,为了能够使该方法能够扩展任意类型的数组,这里的入参数组 a 是Object 类型,因此它并不具备数组自身的获取长度的功能,我们只能借用其他方法获取数组长度,这也是方法里需要判断 a 是不是一个数组的原因。
5.7.6 调用任意方法
相关方法介绍:
所属类 | java.lang.Class<T> | |
---|---|---|
方法 | public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException | |
描述 | 返回一个 Method 对象,它反映当前 Class 对象所表示的类或接口的指定公共成员方法。入参 name 用于指定要返回的方法的简称,parameterTypes 为可变参数,是按声明顺序标识该方法形参类型的 Class 对象的一个数组,可省略。 | |
方法 | public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException | |
描述 | 和 getMethod 方法类似,不同的是该方法不能获取继承父类的方法,但可以获取公共和私有方法。 | |
所属类 | java.lang.reflect.Method | |
方法 | public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException | |
描述 | 调用 Method 类对象表示的底层方法。入参 obj 用于指定要调用的方法所属的对象(这里要传入实例化的对象),args 为调用该方法需要传入的参数,顺序要和方法声明的一致。 |
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Method ml = Employee.class.getMethod("getName");
String n = (String) ml.invoke(harry);
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
double s = (Double) m2.invoke(harry);
注意:
invoke 方法的参数和返回值必须是 Object 类型的,这就意味着必须进行多次的类型转换,这样会使编译器错过检查代码的机会,且性能相比于直接调用方法有着明显的差距。
有鉴于此, 建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8中的 lambda 表达式(第 6 章中介绍)。 特别要重申: 建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。
5.8 继承的设计技巧
1. 将公共操作和域放在超类。
2. 不要使用受保护的域。(会破坏封装性)
3. 使用继承实现“ is-a” 关系。(注意分辨是否是 is-a 关系,避免造成滥用)
4. 除非所有继承的方法都有意义, 否则不要使用继承。
5. 在覆盖方法时, 不要改变预期的行为。(不要偏离最初的设计
想法)
6. 使用多态,而非类型信息。
if (x is of type1) action1(x); else if (x is of type2) action2(x);
无论何时,对于上述代码,我们都应该考虑使用多态性,action1 和 action2 如果是相同的概念, 就应该为这个概念定义一个方法, 并将其放置在两个类的超类或接口中,然后, 就可以调用x.action();
7. 不要过多地使用反射。
第 6 章 接口、lambda 表达式与内部类
6.1 接口
首先给出一个接口的实例:
public interface InterfaceDemo {
/* 接口中只能有静态常量,不能有实例域。 */
int CONSTANT_ONE = 10;
/* 接口中的方法默认为 public,因此我们可以省略权限修饰符。 */
String generalMethod(String param);
/* JavaSE 8 之后允许在接口中提供 static、default 两类简单的方法,之前是不允许的。 */
default String dafaultMethod() {
return "This is a default method. ";
}
static String staticMethod() {
return "this is a static method.";
}
}
上述接口编译后的 .class 文件经过反编译后的内容为:
public interface InterfaceDemo
{
public static final int CONSTANT_ONE = 10;
String generalMethod(String paramString);
default String dafaultMethod() { return "This is a default method. "; }
static String staticMethod() { return "this is a static method."; }
}
结合上述示例,下面介绍接口的几个特点。
1. 常量:接口中可以定义常量。
从上面 .class 文件的内容我们可以看出,接口中的域自动的被加上了 static 和 final 修饰符,因此接口中的域只能存在常量。
2. 接口中的所有方法自动地属于 public 方法。
在 JavaSE 8 之前,接口中是 不允许实现方法的, 之后则 允许提供 static、default 两类实现的方法。由于接口没有实例域,因此提供的简单方法并不能操作实例域。
接口里的所有方法自动被认为是 public 方法,因此我们不能使用除 public 外的其它权限修饰符来修饰接口中的方法(接口中提供的实现过的 default 方法例外)。
2. 实现接口时必须给实现的方法指定 public 修饰符
虽然接口中的方法会自动的被认为是 public 方法,但在实现类中,我们必须显式的为实现的方法给出 public 修饰符。
在原书中,为解释接口,给出了一个使用 Arrays 工具类的 sort 方法对 Employee 数组进行排序的例子。使用 sort 方法对对象数组进行排序需要待排序对象所属的类实现 Comparable 接口,顺序则通过实现的 compareTo 方法进行比较后决定。那么问题来了,排序依赖的是 compareTo 方法呀,为什么不能直接在 Employee 类中给出 compareTo 方法而必须实现 Comparable 接口呢?
上述问题的主要原因在于 Java 程序设计语言是一种强类型语言。在调用方法的时候,编译器将会检查这个方法是否存在。也就是说,编译器必须要确认需要排序的对象数组的每一项所存储的对象都拥有 compareTo 方法。怎么能确认一定拥有 compareTo 方法呢?如果对象数组是一个 Comparable 数组,它就一定会拥有 compareTo 方法,基于此就有了上面的必须实现 Comparable 接口的结果。(注意这只是一种设计方式,并非定理一样的东西。)
6.1.2 接口的特性
1. 接口不是类,不能使用 new 运算符进行实例化。
2. 我们能够声明接口变量。
InterfaceDemo interfaceDemo;
3. 接口变量必须引用实现了接口的类对象。
InterfaceDemo interfaceDemo = new InterfaceImpl();
4. 可以使用 instanceof 检查一个对象是否实现了某一接口。
if(obj instanceof InterfaceDemo){ . . . }
6.1.3 接口与抽象类
接口与抽象类的区别:
1. 抽象类中除了抽象方法外还可以包含抽象方法,接口中只能有抽象方法(自动为 public)
2. 抽象类中的成员域没有限制,接口中只能有常量(自动被 被public static final修饰)。
3. Java 中类和接口只能有一个父类,但可以实现多个接口(一个接口可以继承多个接口)。
4. 抽象类是对一类事物的抽象,接口则是对行为的抽象。一个类继承一个抽象类代表“是不是”的关系,而一个类实现一个接口则表示“有没有”的关系。
Java中引入接口是为了解决不允许多继承的问题。接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
6.1.6 解决默认方法冲突
Java 8 新增了接口的默认方法(上述 default 方法),如果一个类实现了多个接口,而每个接口都有一个相同的默认方法,这时默认方法就起了冲突,此时我们可以采取以下两种方案解决:
1. 创建自己的默认方法,来覆盖重写接口的默认方法。
2. 使用 super 来调用指定接口的默认方法。
详情参阅:https://www.runoob.com/java/java8-default-methods.html
6.2 接口示例
6.2.1 接口与回调
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
6.2.3 对象克隆
变量副本: 当我们为一个包含对象引用的变量建立副本时,该副本和原变量将引用同一个对象,这也就意味着我们改变任何一个变量,两个变量都将受到影响。
克隆: 克隆是创建一个和原对象一样但处于另外一个存储空间的全新对象,也就是说,克隆对象和原对象在克隆完成后就是两个互不影响的对象了。
所属类 | java.lang.Object | |
---|---|---|
方法 | protected Object clone() throws CloneNotSupportedException | |
描述 | 创建一个全新的和原对象初始状态一直的对象,克隆对象和原对象之间不会互相影响,是两个位于不同地址的对象。 |
当我们使用上述 clone 方法进行克隆时,默认状态下该克隆是浅克隆,也就是说如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、 但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来, 原对象和克隆的对象仍然会共享一些信息。示意图如下:
由于 Object 的 clone 方法是 protected 方法,所以我们不能直接调用该方法。如果我们认为一个类需要使用 clone 方法,我们应该让这个类实现 Cloneable 接口,并重新定义 clone 方法,为其指定 public 修饰符。
我们需要注意的是 Cloneable 接口是一个标记接口,其不包含任何方法,只是为了指示实现了该接口的类可以进行 clone 操作。
标记接口不包含任何方法,通常只是为了确保一个类实现了一个或一组特定的方法,它唯一的作用就是允许在类型查询中使用 instanceof。
无论默认 clone 方法的浅拷贝是否能够满足需求,我们都需要实现 Cloneable 接口,将 clone 重新定义为 public, 再调用 super.clone()。
浅拷贝示例:
class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException{
return (Employee) super.clone();
}
}
重新定义 clone 方法实现深拷贝示例:
class Employee implements Cloneable{
public Employee cloneO throws CloneNotSupportedException{
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
6.3 lambda 表达式
6.3.2 lambda 表达式的语法
lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。其语法为(参数)-> 表达式
,示例如下:
(String first, String second) -> first.length() - second.length()
如果参数的类型是确定的,我们可以省略其类型,示例如下:
(first, second) -> first.length() - second.length()
如果没有参数,我们需要给出空括号,示例如下:
() -> System.out.println("Hello . . . ")
如果要完成的计算无法在一个表达式中完成,我们将相关代码放到{}
中,但需要显式的给出return
语句,示例如下:
(String first, String second) -> {
. . .
. . .
return ***;
}
lambda 表达式中的表达式部分如果使用
{ }
进行包裹,则一定要显示的给出return
语句!
如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值,这是不合法的。 例如,(int x)-> { if (x >= 0) return 1; } 就不合法,我们必须给出 else 分支的返回值。
6.3.2 函数式接口
1. 什么是函数式接口?
在 Java 中,有且仅有一个抽象方法(可以有多个非抽象方法)的接口被称为函数式接口( Functional Interface ),我们只能为函数式接口提供一个 lambda 表达式。
2. lambda 表达式怎么理解?
Lambda 表达式 (lambda expression) 是一个匿名函数,在 Java 中应该称其为匿名方法。参考上面给出的 lambda 表达式的示例,我们可以将各部分和方法的构成进行对应,其中 lambda 表达式( )
中的参数为方法的入参,lambda 表达式中的表达式为方法体。我们可以使用 lambda 匿名调用方法。
3. 是所有的方法都能使用 lambda 表达式匿名调用吗?
当然不是,lambda 表达式的目标只能是函数式接口,也就是说,它只能匿名调用函数式接口的方法,其中表达式区域就是对目标接口方法的实现。
比如 Arrays 工具类中的 sort 方法可以对数组中的元素进行排序,当排序对象是非基本数据类型时,我们需要给 sort 方法提供一个比较器 Comparator,这个比较器就是只有一个抽象方法的接口,我们可以为其提供一个 lambda 表达式。
public static <T> void sort(T[] a, Comparator<? super T> c)
Comparator 是一个接口,它提供了一个用于比较的方法 compare。
int compare(T o1,T o2)
我们可以使用 lambda 表达式实现并匿名调用 Comparator 接口中的 compare 方法:
String[] a = {"1","4","3","2"};
//排序
Arrays.sort(a, (first, second)->Integer.parseInt(first)-Integer.parseInt(second));
//遍历
Arrays.stream(a).forEach((x)->System.out.println(x));
sort 方法需要 Comparator 类型的对象做入参的原因是为了明确排序的比较规则,我们这里使用 lambda 表达式相当于直接给出了两个待比较对象即数组里的两个待比较项的比较规则,之后 sort 会不断的使用我们给出的规则进行比较,直至数组有序。
特别注意:当且仅当接口只有一个抽象方法(函数式接口)时我们才能为其提供 lambda 表达式!!!
Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。其中一个接口 BiFunction<T, U, R> 描述了参数类型为 T 和 U 而且返回类型为 R 的函数。我们可以将上述字符串比较的 lambda 表达式保存在这个类型的变量中,当然 sort,方法并不能接收一个 BiFunction 类型的参数,我们要根据实际需要的参数类型来决定是否使用 BiFunction 。将 lambda 表达式放入 BiFunction 类型变量的示例如下:
BiFunction<String, String, Integer〉comp = (first, second)->Integer.parseInt(first)->Integer.parseInt(second));
在 java.util.function 包中有一个尤其有用的接口 Predicate:
public interface Predicate<T> { boolean test(T t); . . . }
ArrayList 类有一个 removelf 方法, 它的参数就是一个 Predicate。这个接口专门用来传递 lambda 表达式。例如,下面的语句将从一个数组列表删除所有 null 值:
list.removelf(e -> e == null);
6.3.4 方法引用
上面介绍的 lambda 表达式需要我们在表达式体{ }
中手动定义我们的处理规则,但有时候我们直接调用现有的方法就能满足需求,我们能不能省略 lambda 表达式中的 ( )
和{ }
省略而直接去引用方法呢?
我们可以使用::
操作符直接引用方法,书中给出的第一个示例如下:
Timer t = new Timer(1000, event -> System.out.println(event));
可以写成:
Timer t = new Timer(1000, System.out::println);
表达式 System.out::println 是一个方法引用( method reference ), 它等价于 lambda 表达式x -> System.out.println(x)
。
也就是说,当函数式接口的实现只需要调用一个现有方法就能满足需求时,我们直接给出现有方法的引用即可。如我们想对字符串排序, 而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgnoreCase);
注意:类似于 lambda 表达式, 方法引用不能独立存在,总是会转换为函数式接口的实例。
引用方法时,我们要用::
操作符分隔方法名与对象或类名。主要有 3 种情况:
1. object::instanceMethod——object 是实例对象
1. Class::staticMethod——Class 是类名
1. Class::instanceMethod——Class 是类名
在前 2 种情况中, 方法引用等价于提供方法参数的 lambda 表达式。如System.out::println
等价于x -> System.out.println(x)
,Math::pow
等价于(x,y)->Math.pow(x, y)
。
对于第 3 种情况, 第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase
等同于(x, y)-> x.compareToIgnoreCase(y)
;
我们还可以在方法引用中使用 this 参数。例如,this::equals 等同于 x-> this.equals(x)。同样的,我们还可以使用 super,如 super::instanceMethod。这里的 this 和 super 意义与之前一致。
6.3.5 构造器引用
构造器引用与方法引用很类似,只不过方法名为 new。例如, Person::new 就是 Person 构造器的一个引用。至于引用的是哪一个构造器,则需要取决于上下文,书中给出的实例如下:
ArrayList<String> names = . . .;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.col1ect(Collectors.toList());
map 方法的入参是一个 Function 类型的接口,这个 Function 是功能接口,也就是说我们可以使用 lambda 表达式或方法引用为其制定功能,上述例子使用构造器引用为功能接口赋予了实例化对象的功能。至于上述例子调用哪个构造器,则是根据上下文推导出这是在对一个字符串调用构造器(names 是一个字符串数组)。
有关 stream、 map 和 collect 方法的详细内容,将在《Java 核心技术卷 Ⅱ》的读书笔记中给出。
可以用数组类型建立构造器引用。例如,int[]::new
是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda 表达式x-> new int[x]
。
Java 有一个限制,无法构造泛型类型 T 的数组。表达式 new T[n] 会产生错误,因为这会改为 new Object[n]。 Stream 接口有一个 toArray 方法可以返回 Object 数组:Object[] people = stream.toArray();
,但用户希望得到一个 Person 引用数组,而不是 Object 引用数组。我们可以把 Person[]::new 传入 toArray 方法:Person口 people = stream.toArray(Person[]::new):
,这样 toArray 方法调用这个构造器将得到一个正确类型的数组,然后填充这个数组并返回。
6.3.6 变量作用域
lambda 表达式可以捕获外围作用域中变量的值,即在 lambda 表达式中可以引用表达式外部区域定义的变量。但是在引用外围变量时我们要注意一下几点:
1. 不能在 lambda 表达式内部改变引用的变量值,因为这样会导致在并发执行多个动作时不安全。
2.lambda 表达式中捕获的变量必须实际上是最终变量,即不能引用在外围可能发生改变的变量,。
3. 在 lambda 表达式中不能声明与外围局部变量同名的变量。
lambda 表达式是一个闭包。
闭包:闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
6.3.7 处理 lambda 表达式
如果想要使用 lambda 表达式,我们需要提供一个函数值接口,下面是从书中截取的常用函数式接口列表:
下面将给出直接从书中截取的基本类型 int、 long 和 double 的 34 个可能的规范列表,我们应该尽量使用这些特殊化规范来减少自动装箱。
最好使用表 6-1 或表 6-2 中的接口。 例如, 假设要编写一个方法来处理满足某个特定条件的文件。对此有一个遗留接口
java.io.FileFilter
, 不过最好使用标准的Predicate<File>
, 只有一种情况下可以不这么做, 那就是你已经有很多有用的方法可以生
成 FileFilter 实例。
如果我们自己设计只有一个抽象方法的接口,可以用@FunctionalInterface
注解来标记这个接口。 这样做有两个优点。 一是如果你无意中增加了另一个非抽象方法, 编译器会产生一个错误消息。 二是 javadoc 页里会指出你的接口是一个函数式接口。
当然并不是必须使用注解定义, 任何有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface 注解确实是一个很好的做法。
6.3.8 再谈 Comparator
Comparator 接口包含很多可用于 lambda 表达式和方法引用的静态方法,我们可以通过这些方法方便的创建比较器。
所属类 | Interface Comparator<T>(属于java.util 包) | |
---|---|---|
方法 | static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor) | |
描述 | 这是一个“键提取器”函数,它将从类型 T 中提取用于比较的排序键(比如按名称排序,则抽取出名称作为排序键),然后返回由该排序键进行比较的比较器。 如果指定的函数也是可序列化的,则返回的比较器是可序列化的。 | |
方法 | default Comparator thenComparing(Comparator<? super T> other) | |
描述 | 我们可以使用该方法在原比较器的基础上添加新的比较规则,最终创建一个由原比较器和新比较器组成的字典顺序比较器。 在原比较器比较结果为 0(即相等)时将使用新的比较器 other 进行比较。 |
例如,有一个 Person 对象数组,我们可以使用 comparing 静态方法实现按名称进行排序。
Arrays.sort(people, Comparator.comparing(Person::getName)) ;
我们还可以将比较器和 thenComparing 方法串联起来使用:
Arrays.sort(people,
Comparator.comparing(Person::getlastName).thenComparing(Person::getFi rstName)) ;
getlastName 方法是得到 people 对象的姓,上述比较器联合 thenComparing 方法后,将先比较 people 对象的姓,若相等再使用 thenComparing 方法返回的比较器进行比较,即比较名。
我们还可以为 comparing 和 thenComparing 方法指定比较器,下面给出一个根据人名长度进行排序的例子。
Arrays.sort(people, Comparator.comparing(Person::getName,
(s, t) -> Integer.compare(s.1ength(), t.length())));
comparing 和 thenComparing 方法都有变体形式,可以避免 int、 long 或 double 值的装箱,示例如下:
Arrays.sort(people, Comparator.comparingInt(p -> p.getName() -length()));
为 comparing 和 thenComparing 方法指定排序键使用的方法是有可能返回 null 的,此时我们应该使用 nullsFirst 和 nullsLast 适配器来保证遇到 null 值时将这个值标记为小于或大于正常值,而不是抛出异常。
Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst((s, t) -> Integer.compare(s.1ength(), t.length())));
nullsFirst 方法需要一个比较器,其作用是在比较过程中遇到空值时将该空值标记为小于正常值(nullsLast 标记为大于正常值)。对于非空值则使用为 nullsFirst 方法提供的比较器进行比较。
naturalOrder 方法可以为任何实现了 Comparable 的类建立一个比较器。我们可以使用 Comparator.<String>naturalOrder() 方法为 nullsFirst 提供比较器,示例如下:
Arrays.sort(people, Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(Comparator.naturalOrder())));
静态 reverseOrder 方法会提供自然顺序的逆序。要让比较器逆序比较, 可以使用 reversed 实例方法。例如 naturalOrder().reversed() 等同于 reverseOrder()。
6.4 内部类
内部类(inner class)是定义在另一个类中的类。使用内部类的原因有如下三点:
1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。
2. 内部类可以对同一个包中的其他类隐藏起来。
3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类(anonymous)比较便捷。
6.4.1 使用内部类访问对象状态
在原书上给出了如下示例:
public class TalkingClodk {
private int interval;
private boolean beep;
public TalkingClodk(int interval, boolean beep) {
this.interval = interval;
this.beep = beep;
}
public void start(){
}
public class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone,the time is " + new Date());
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
}
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。 为了保证内部类能够访问创建它的外围类对象的数据,内部类对象有一个指向创建它的外部类对象的隐式引用。书上给出的示意图如下:
这个隐式的引用在内部类的定义中是不可见的。如上图,我们假设对外部对象的引用表示为 outer (后面将介绍规范的引用语法),那么在内部类 actionPerformed 方法中的if(beep)
就等价于if(outer.beep)
。为验证这一说法,我们对编译生成的 class 文件进行反编译,可以看到如下的内容:
public class TalkingClodk {
private int interval;
private boolean beep;
public TalkingClodk(int paramInt, boolean paramBoolean) {
this.interval = paramInt;
this.beep = paramBoolean;
}
public void start() {}
public class TimePrinter
implements ActionListener {
public void actionPerformed(ActionEvent param1ActionEvent) {
System.out.println("At the tone, the time is " + new Date());
if (TalkingClodk.this.beep) Toolkit.getDefaultToolkit().beep();
}
}
}
TalkingClodk.this 表示对外围类的引用。
我们可以采用outerObject.new InnerClass {construction parameters)
的格式更加明确地编写内部对象的构造器。
我们对 TalkingClodk.java 文件编译后会发现生成了两个 class 文件,分别是 TalkingClodk.class 和 TalkingClodk$TimePrinter.class 其中 TalkingClodk$TimePrinter.class 是编译器将内部类翻译成用 $ (美元符号)分隔外部类名与内部类名的常规类文件(虚拟机则对此一无所知)。
6.4.2 内部类的特殊语法规则
对于一个公有内部类,我们是可以对其进行实例化的。根据实例化的位置不同,其语法格式也不同,下面个将给出示例。
- 在外部类外部 创建非静态内部类
语法: 外部类.内部类 内部类对象 = new 外部类().new 内部类();
Outer.Inner in = new Outer().new Inner();
- 在外部类外部 创建静态内部类
语法: 外部类.内部类 内部类对象 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
- 在外部类内部创建内部类
语法:和创建普通对象一样。
Inner in = new Inner();
6.4.4 局部内部类
我们可以在一个方法中定义局部类,示例如下:
public void start(){
//声明局部内部类
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + new Date());
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
局部类不能用 public 或 private 访问说明符进行声明。它的作用域被定在声明这个局部类的块中,并且对外部完全隐藏,也就是说,除了声明这个局部类所在的块之外其它任何地方都无法访问这个类。
局部类不仅能够访问包含它们的外部类, 还可以访问被 final 修饰的局部变量。
6.4.6 匿名内部类
不定义名称,直接实例化的对象就是匿名内部类,示例如下:
public void start(int interval, boolean beep) {
//创建一个实现了 ActionListener 接口的类对象,这个对象是没有命名的。
//虽然表面来看 listener 就是这个新建对象的名称,但 ActionListener 是一个接口
// 不可能被实例化,我们实例化的是这个接口的实现类的对象,因此这就是一个匿名内部类。
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval, listener);
t.start();
}
由于构造器的名字必须与类名相同, 而匿名类没有类名, 所以, 匿名类不能有构造器,需要使用超类的构造器进行构造。
双括号初始化:
invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});
上面创建了一个匿名数组列表,这种方式被称为双括号初始化。我们需要注意
{{ add("Harry"); add("Tony"); }}
这一部分,外层括号建立了 ArrayList 的一个匿名子类。 内层括号则是一个对象构造块。
6.4.7 静态内部类
使用 static 修饰的内部类被称为静态内部类。
在内部类不需要访问外围类对象的时候, 应该使用静态内部类。
与常规内部类不同,静态内部类可以有静态域和方法。
声明在接口中的内部类自动成为 static 和 public 类。
6.5 代理
就像字面意思一样,代理就是代为处理,类似于现实生活中的代购。当我们想买的东西不方便亲自购买时,我们可以作为委托者委托给代购进行购买,之后由代购将物品给我们,在这个过程中,代购去购买的过程是相同的,不同的是委托者所委托购买的物品种类。相似的,当多个类(委托者)要执行某个流程相同的操作时,我们就可以创建一个“代购”(代理对象)来代理,之后由代理对象将结果交给委托者。
根据加载被代理类的时机不同,可将代理分为静态代理和动态代理,在这里我们只给出动态代理的简介。
动态代理之所以叫动态代理,是因为我们可以使用代理类在程序运行时动态的创建一个实现了一组给定接口的全新的类。
代购重要的就是其持有的购买渠道,相对应的,代理类重要的就是其所持有的方法,而给定接口就是为了赋予代理类对应的方法。
下面将分步给出创建一个代理对象的示例:
1. 定义调用处理器
调用处理器是实现了 InvocationHandler 接口的类对象,是被代理对象与代理对象之间的桥梁,在创建代理对象时将作为必要的入参。在 InvocationHandler 接口中只有一个 invoke 方法,调用处理器需要实现这一方法,给出处理调用的方式。
所属类 | Interface InvocationHandler(属于 java.lang.reflect 包) | |
---|---|---|
方法 | Object invoke(Object proxy, Method method, Object[] args) throws Throwable | |
描述 | 处理代理实例上的方法调用并返回结果。当在与其关联的代理实例上调用方法时,将在调用处理程序上调用此方法。 proxy:代理对象。 method:调用的方法。 args:被调用方法的入参。 |
无论何时调用代理对象的方法, 调用处理器的 invoke 方法都会被调用, Method 对象和原始的调用参数也会被传递到该方法中。
//定义一个实现了InvocationHandler 接口的调用处理器
public class TranceHandler implements InvocationHandler {
//被代理对象的引用
private Object target;
//调用处理器的构造器
public TranceHandler(Object target) {
this.target = target;
}
/**
* 在invoke方法中给出处理调用的方式
* @param proxy 代理对象
* @param method 被调用的方法
* @param args 被调用方法的入参
* @return 调用处理器的处理结果
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print(target);
System.out.print("."+ method.getName()+"(");
if(args != null){
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if (i < args.length-1) System.out.print(", ");
}
}
System.out.println(")");
//调用被代理对象的方法,target 为被代理的对象,method 为需要调用的方法的对象。
return method.invoke(target,args);
}
}
2. 创建调用处理器
//obj 为被代理对象的引用
InvocationHandler stuHandler = new TranceHandler(obj);
3. 生成代理对象
Object proxy = Proxy.newProxyInstance(null,new Class[]{Comparable.class},handler);
所属类 | Class Proxy (属于java.lang.reflect 包) | |
---|---|---|
方法 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException | |
描述 | 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。 loader:类加载器,可以用 null 表示使用默认的类加载器。 interfaces:一个 Class 对象数组, 每个元素都是需要实现的接口。 h:一个调用处理器。 |
我们可以看到,在创建代理对象时需要提供一组待实现的接口,这组接口就是“代购”的“购买渠道”,因此代理对象会实现这些指定接口。而被代理的对象想要进行代理则必须有使用“”购买渠道”的需求,这也就要求被代理对象也要实现这组接口中的至少一个。
既然代理对象最重要的是“购买渠道”,那我们就必须清楚它拥有哪些“渠道”,也就是拥有哪些方法。详情如下:
1. 指定接口所需要的全部方法
2. Object 类中的全部方法, 例如, toString、 equals 等。
注意:当代理对象的上述任一方法被调用时都会执行调用处理器中的 invoke 方法。
至此,一个代理对象就创建完成了。
代理对象虽然也实现了指定的接口,但当调用代理对象的这些方法(“购买渠道”)时会去调用调用处理器的 invoke 方法,然后根据 invoke 方法中给出的调用处理逻辑调用被代理对象对应这些指定接口的方法。在上述代码中,指定的接口是 Comparable ,也就是说当调用代理对象的 compareTo 方法时会执行调用处理器 TranceHandler 的 invoke 方法,然后在 invoke 中指定调用被代理对象 obj 的 compareTo 方法。
下面将给出一个使用上面定义的调用处理器的完整示例代码:
public class ProxyTest {
public static void main(String[] args) {
Object[] element = new Object[1000];
for (int i = 0; i < element.length; i++) {
Integer value = i+1;
InvocationHandler handler = new TranceHandler(value);
Object proxy = Proxy.newProxyInstance(null,new Class[]{Comparable.class},handler);
element[i] = proxy;
}
Integer key = new Random().nextInt(element.length)+1;
int result = Arrays.binarySearch(element, key);
if (result > 0) System.out.println(element[result]);
}
}
输出结果为:
500.compareTo(670)
750.compareTo(670)
625.compareTo(670)
687.compareTo(670)
656.compareTo(670)
671.compareTo(670)
663.compareTo(670)
667.compareTo(670)
669.compareTo(670)
670.compareTo(670)
670.toString()
670
6.5.3 代理类的特性
1. 代理类是在程序运行过程中创建的。一旦被创建, 就变成了常规类, 与虚拟机中的任何其他类没有什么区别。
2. 所有的代理类都扩展于 Proxy 类。一个代理类只有一个实例域,即调用处理器,它被定义在 Proxy 的超类中。
3. 所有的代理类都覆盖了 Object 类中的方法 toString、 equals 和 hashCode。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的 invoke。Object 类中的其他方法(如 clone 和 getClass) 没有被重新定义。
4. 没有定义代理类的名字,Sun 虚拟机中的 Proxy类将生成一个以字符串 SProxy 开头的类名。
5. 对于特定的类加载器和预设的一组接口来说, 只能有一个代理类。 也就是说, 如果使用同一个类加载器和接口数组调用两次 newProxylustance 方法的话, 那么只能够得到同一个类的两个对象。
6. 我们还能使用如下方法生成代理类:
//该部分代码摘录自下方提供的博客
//创建一个InvocationHandler对象
InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
//使用 Proxy 类的 getProxyClass 静态方法生成一个动态代理类 stuProxyClass
Class<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[] {Person.class});
//获得stuProxyClass 中一个带InvocationHandler参数的构造器constructor
Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);
//通过构造器constructor来创建一个动态实例stuProxy
Person stuProxy = (Person) cons.newInstance(stuHandler);
7. 代理类一定是 public 和 final。 如果代理类实现的所有接口都是 public, 代理类就不属于某个特定的包;否则, 所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
8. 可以通过调用 Proxy 类中的 isProxyClass 方法检测一个特定的 Class 对象是否代表一个代理类。
更多有关代理的信息可参考博客:https://blog.csdn.net/liguangix/article/details/80858807