继承
继承是在现有类的基础上创建新类的过程。
一些要点:
- super 不同于 this 引用,super 不是对象的引用,而是绕过动态查找方法并调用特定方法的指令。
- 将子类对象的引用赋值给父类对象时(B 继承 A, B b = new B(); A a = b;),在父类变量上调用一个方法时,虚拟机会查看对象的实际类型,并且定位方法的版本。这个过程就是动态方法查找。
- 抽象类不同于接口(只有静态常量和抽象方法和静态方法),抽象类可以拥有实例变量和构造函数。但是不能构造抽象类的实例,但是可以将具体子类的对象的引用赋值给抽象类变量。
- 继承和默认方法,被继承类和被实现接口有同名方法时,父类的实现总是赢过接口的实现,类比接口优先。
- 带 super 的方法表达式,方法表达式有如下形式: object::instanceMethod。使用对象 super 代替对象引用同样是有效的,方法表达式 super::instanceMethod 用 this 作为目标,并且调用父类版本的给定方法。
匿名子类:你可以拥有实现接口的抽象类,也可以拥有继承父类的匿名类。这方便于调试。如双括号初始化,运用内部类语法来初始化(invite是一个方法名):
invite(new ArrayList<String>() {{add("Harry");("Bob");add("CC");}});
双重大括号,外面的大括号创建的是 ArrayList
Object
终极父类
Java 中的所有类都直接或间接地继承自 Object 类。 Object 类定义了适合于任何对象的方法:
- String toString()
- boolean equals(Object other)
- int hashCode()
- class<?> getClass()
- protected Object clone()
- protected void finalize()
- wait、notify、notifyAll
toString 方法
- 它返回一个对象的字符串描述值,默认该字符串是类名称和哈希码。
- 数组从 Object 继承了 toString 方法。输出的字符串包含数组的类型,只是形式比较老旧。比较好的方式是调用 Arrays.toString(),打印多维数组使用 deepToString。
equals 方法
- equals 方法用来测试一个对象是否被认为与另一个对象相等。
- 只有在基于状态的想等性测试中,才有必要覆盖 equals 方法。在这种测试中,只有两个对象拥有同样的内容时,则认为他们是相等的。
- 无论何时覆盖 equals 方法,均必须同时提供一个兼容的 hashCode 方法。
在 equals 方法中,有一些你需要知道的常规步骤:
- 一般认为两个相等的对象时完全相同的,而且这个检测耗费很小。
- 当与 null 比较时,所有 equals 方法都要返回 false。
- 由于重载 Object 类的 equals 方法,参数是 Object 类型,需要转换为实际的类型,在转换之前用 getClass 方法或 instanceof 操作进行类型检查。
- 最后比较实例变量。对于基本类型使用 == 操作符。但对于 double 类型,如果担心正负无穷大或者 NaN, 那么使用 Double.equals 方法。对于对象使用 Objects.equals 方法,一个 null 安全的 equals 方法。如果 x 为空,则 Objects.equals(x, y) 方法会返回 false, 而 x.equals(y) 方法会抛出异常。
其它:
- 如果有数组类型的实例变量,则使用静态方法 Array.equals 来检查数组是否有相等的长度,以及相应的数组元素是否相等。
- 在子类中定义 equals 方法时,先要调用父类的 equals 方法。如果父类的检查没有通过,那么对象肯定不相等。如果父类的实例变量都相等,在比较子类的实例变量。
hashCode 方法
- 哈希码(hash code)是个整数,来源于对象。哈希码应该是杂乱无序的——如果 x 和 y 是两个不相等的对象,那么 x.hashCode() 和 y.hashCode() 很可能不同。
- hashCode 和 equals 方法必须是兼容的,如果 x.equals(y),那么 x.hashCode = y.hashCode。
- 由于 Object.equals 用来检测相同的对象,唯一要注意的相同对象拥有同样的哈希码。
- 如果重定义了 equals 方法,则也需要重定义 hashCode 方法,以兼容 equals 方法。如果不那样做,则当用户将你的类插入哈希集合或者 HashMap 时,它们可能会丢失。
- 可以直接联合各个实例变量的哈希码,Objects.hash(…) 方法的参数是个可变参数,该方法会计算每个参数的哈希码,并将它们组合起来,这个方法时空指针安全的。
- 接口决不能重新定义 Object 类的方法成为默认方法,由于类比接口优先,这样的方法永远会被使用。
clone 方法
- clone 方法的目的是创建一个“克隆”的对象——拥有与原对象相同的状态的不同对象。如果你改变了其中一个,则另一个不会改变。
- Object.clone 方法做了一个浅拷贝。它简单地从原对象拷贝所有实例变量到被拷贝对象里。如果实例变量都是基本类型或者不会改变,那没为题。但是,如果他们不是,原对象和克隆对象将共享可变的状态,这会有问题。
一般情况下,当你实现一个类时,你需要考虑如下情况:是否提供 clone 方法。如果不提供 clone 方法,那么继承自父类的 clone 方法是否可以接受,如果继承自父类的 clone 方法不可接受,就需要提供实现深拷贝的 clone 方法。
对于第一个选项,什么也不用做。你的类将继承 clone 方法,但是由于它是 protected 的,所以没有用户可以调用它。
对于第二个选项,(1)你的类必须实现 Cloneable 接口。这是一个没有任何方法的接口,称作标签(tagging 或 marker)接口。 Object.clone 方法在执行浅拷贝之前,会检查这个接口是否被实现,如果没有,会抛出 cloneNotSupportedException 异常。(2)将 protected 改为 public,并改变返回类型。(2)处理cloneNotSupportedException异常,是一个检查异常要么声明它,要么捕获它。如果你的类声明为 final ,你可以捕获它。否则就声明这个异常,因为有可能子类想重新捕获这个异常。
枚举
定义
枚举类型(enum type)是指由一组固定的常量组成合法的类型。Java中由关键字enum来定义一个枚举类型。下面就是java枚举类型的定义。
public enum Size {SMALL, MEDIUM, LARGE}
- 由于每个枚举类型都有固定的实例集,因此对于枚举类型的值来说,你永远不需要 equals 方法,直接使用 == 比较。
- 不需要提供 toString 方法,它会自动产生枚举对象的名称(SMALL,MEDIUM…)。
每个枚举类型都有一个静态方法 values,该方法返回一个按照其声明次序排列的包含所有枚举实例的数组。
Size[] allValues = Size.values(); Log.d("MainActivity", Arrays.toString(allValues)); //结果 D/MainActivity: [SMALL, MEDIUM, LARGE] //遍历 for (Size s : Size.values()){ Log.d("MainActivity", s.toString()); }
构造函数、方法和域
可以给枚举类型添加构造函数、方法和域。例如:
public enum Size {
SMALL("S"), MEDIUM("M"),LARGE("L");
private String mCode;
Size(String code){
this.mCode = code;
}
public String getmCode() {
return mCode;
}
}
每个枚举类型实例保证只被构造一次。枚举类型的构造函数总是私有的,可以省略。
枚举类可以拥有静态成员,但要消息构造次序。由于枚举常量在静态成员之前构建,所以你不能在构造函数里面引用任何静态成员。解决办法是在一个静态初始化块中进行初始化工作。
switch 枚举对象
public void chooseSize(Size size){
switch (size){
case SMALL:
break;
case MEDIUM:
break;
case LARGE:
break;
}
}
运行时类型信息和资源
在 Java 中,你可以在程序运行时查询一个对象属于哪个类。这有时很有用,例如在 equals 和 toString 方法的实现中。此外,还可以查出类是如何加载的,并加载被称为资源的相关数据。
Class 类
假设你有一个 Object 类型的变量,它引用了某个对象,你想知道关于该对象的更多信息,比如它属于哪个类。
Object obj = ...;
Class<?> cl = obj.getClass();
//得到 Class 对象
String className = "java.util.Scanner";
try {
Class<?> cls = Class.forName(className);
Log.d("MainActivity", cls.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Class.forName 方法主要用于构造那些可能在编译时还不被知晓的类的 Class 对象。
.class 后缀也可以被用来获取其他类型信息;
Class<?> cl1 = String[].class;
Class<?> cl2 = Runnable.class;
Class<?> cl3 = int.class;
Class<?> cl4 = void.class;
Arrays 在 java 中属于类,但接口、接本类型和 void 都不是类。 Class 这个名称有一些不合适——Type 更准确。
Class 类的一个有用服务就是定位引用程序可能需要的资源,比如配置文件或者图片。如果将资源与类文件放在同一位置,则可以像这样给文件打开一个输入流:
InputStream inputStream = A.getClass().getResourceAsStream("a.txt");
参数也可以放绝对路径。
加载器
类加载器
一个类加载器负责加载字节流,并且在虚拟机中将它们转化为一个类或者接口。当执行 Java 程序时,至少会涉及三个类加载器。
- bootstrap 类加载器会加载 Java 类库(一般来自 jre/lib/rt.jar文件)。
- 扩展类加载器从 jre/lib/ext 目录中加载“标准扩展库部分”。
- 系统类加载器加载应用程序类。它定位于 Classpath 中目录和 JAR 文件的类。
其他:
- 在Java 实现中,扩展类和系统类加载器都是在 Java 中实现的。它们都是 URLClassLoader 类的实例。
- 通过创建自己的 URLClassLoader 实例,你可以从 classpath 以外的目录或者 JAR 文件中加载类。这常用来加载插件。
- URLClassLoader 从文件系统中加载类,如果想从其他地方加载类,需要编写自己的加载器,需要实现的唯一方法是 findClass。
上下文加载器
大部分时候,不必担心类加载器进程。当一个类被其他类需要时,它会被透明地加载。但是,如果某个方法动态地加载类,并且调用该方法的类又是被另一个类加载器加载的,那么就会有问题。一个办法是使用当前线程的上下文加载器。
主线程的上下文加载器是系统加载器。
服务加载器
ServiceLoader 类使得加载那些遵从共同接口的插件变得容易。
反射
反射机制允许程序在运行时检查任意对象的内容,并调用它们任意的方法。这个功能对实现一些工具,例如对象关系映射或者 GUI 构建工具,是有用的。
枚举类成员
在 java.lang.reflect 包中有三个类,Field类、 Method类和 Constructor类分别描述了一个类的域、方法、构造函数。这三个类都有一个 getName 方法返回成员的名称。
- Field 类有一个 getType 方法,返回一个 Class 类型的对象。Method 和 Constructor 类有可以输出参数的类型信息的方法,并且 Method 类还拥有可以输出返回值类型信息的方法。
- 这三个类都有一个 getModifiers 方法,返回一个整数,该整数比特位信息(0 或 1)描述了方法所使用的修饰符(比如 public 或 static)。
- Class 类的 getFields、getMethod、getConstructors 方法会返回该类支持的公有域、方法、和构造参数的数组,还包括从其他地方继承的公有成员。通过 getDeclaredFileds、getDeclaredMethods 和 getDeclaredConstructors 方法,可以分别得到由类中声明的所有域、方法和构造函数组成的数组,包括私有的、包的和受保护的成员,但不包括父类成员。
- Method 和 Contructor 的共同父类——Executable 类的 getParameters 方法返回一个描述方法参数信息的参数对象数组。
打印一个类的所有方法的示例:
public void printMethod(String className){
try {
Class<?> cl = Class.forName(className);
while (cl != null){
for (Method m : cl.getDeclaredMethods()){
System.out.println(m.getModifiers() + " " +
m.getReturnType().getCanonicalName() + " " +
m.getName() + " " +
Arrays.toString(m.getTypeParameters()));
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
对象检查
Field 对象可以被用来读写对象的域。
例如:枚举一个对象的所有域内容:
public void getFieldValue(Object obj){
for (Field f : obj.getClass().getDeclaredFields()){
//使用私有的Field和Method对象之前让它们是可以访问的。解锁 域
f.setAccessible(true);
try {
Object value = f.get(obj);
System.out.println(f.getName() + ":" + value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
方法调用
Method 对象可以调用一个对象的给定方法。
Method m = ...;
Object result = m.invoke(obj, arg1, arg2,...);
如果方法时静态的,则给初始参数赋予 null 值。
获取方法,可以使用 getMethods 或 getDeclaredMethods 方法,例如:
// Person 中有 setName(String) 这个方法
Person p = new Person();
Method m = p.getClass.getMethod("setName", String.class);
p.invoke(obj, "aaaaa");
对象构造
要使用无参构造函数构造对象,可以直接使用 Class 对象的 newInstance 方法。
Class<?> cl = ...; Object obj = cl.newInstance();
要调用其他构造函数,首先要找到 Constructor 对象,然后调用它的 newInstace 方法。如知道一个类的公有构造函数,参数是一个 int 类型。
Constructor constr = cl.getConstructor(int.class); Object obj = constr.newInstance(43);
使用数组
isArray 方法用来检查给定 Class 对象是否是数组。如果是,则通过 getComponentType 方法返回描述数组元素类型的 Class。
java.lang.reflect.Array方法:
- get()
- set()
- getLength()
- new Instace()
代理
Proxy 类可以在运行时创建实现了给定的接口或者接口集的新类。
参考
Core Java for the Impatient[Cay S.Horstmann]