第三章 接口和lambda表达式
一、接口
1、静态方法与默认方法
工厂方法在接口中非常有意义,可以用静态方法来设计,调用者无需关心接口的实例是哪些类。
public interface Fruit {
//静态方法
static Fruit buy(int price){
Fruit fruit;
if(price > 5) fruit = new Banana();
else fruit = new Apple();
return fruit;
}
String eat();
//默认方法
default String fly(){
return "The fruit fly";
}
}
默认方法主要用于 接口演化 。比如说突然所有的水果会飞了,如果不用默认方法。
那么以前的 Apple 和 Banana 不能编译。或者你没有重新编译类,但如果调用 fly 的方法,会发生 AbstractMethodError 异常。
**默认方法冲突:**如果一个类实现了两个接口,两个接口有名称和参数相同的方法且至少其中一个为默认方法,就必须解决冲突。
解决方法,在实例中覆盖原方法,实现自己的方法,或者委托其中一个。
public class Tomato implements Fruit,Vegetable{
@Override
public String fly() {
//委托其中一个
return Fruit.super.fly();
}
......
}
如果继承父类且实现接口有同样方法,按父类的方法。
2、接口示例
标准 java 类库的四个使用接口的常见情况。
Comparable 接口:
public interface Comparable<T> {
public int compareTo(T o);
}
对数组排序,调用类提供的比较方法。即实现此接口的类,重写了 comparTo 的排序时的比较方法。
Comparator 接口:
public interface Comparator<T> {
int compare(T o1, T o2);
......
}
它的源代码除了 compare 需要实现的方法,还有一堆默认方法。
可以理解它为一个外置的排序规则,实现此方法的类,可以作为一种排序的规则放入 sort() 函数中。使用 lambda 可以更方便的使用此接口。
Runable 接口
public interface Runnable {
public abstract void run();
}
Runnable task = new HelloTask();
Thread thread = new Thread(task);
thread.start();
实现 Runable 接口的实例,可以创建一个新线程执行任务。
UI 回调
public interface EventHandler<T extends Event> extends EventListener {
void handle(T event);
}
javaFX 不太熟,就大二刚学java期末作业写过 GUI ,现在已经忘完了,略过。
二、Lambda表达式
Java中几乎一切都是对象,没有函数类型,作为替代,函数被表达成对象,也就是实现了特定接口类的实例,lambda表达式以一种便捷的语法来创建这样的实例。
1、方法引用
list.forEach(x-> System.out.println(x));
list.forEach(System.out::println);
方式 | 代码 | 等同 | 描述 |
---|---|---|---|
类::实例方法 | (x,y)->x.compareToIgnoreCase(y) | String::compareToIgnoreCase | 第一个参数边为方法的接收者,其它参数传递给方法 |
类::静态方法 | x->Objects.isNull(x) | System.out::println | 所有的参数传递给静态方法 |
对象::实例方法 | x-> System.out.println(x) | System.out::println | 给定对象上调用方法,参数传递给实例方法 |
多个重名重载方法时,编译器会从上下文找到匹配的那个。在方法引用中可以捕获 this 与 super。
2、构造函数引用
String[] str = {"a","b"};
Stream<Apple> stream = Arrays.stream(str).map(Apple::new);
Object[] objects = stream.toArray();
Apple[] apples = stream.toArray(Apple[] ::new);
利用流构造一个对象数组
3、使用lambda表达式
lambda的内容太多了,以后单开一篇细写吧。。
4、局部类与匿名类
private static Random generator = new Random();
public static IntSequence randomInts(int low,int high){
//内部类
class RandomSequence implements IntSequence{
public int next() {return low+generator.nextInt(high - low +1);}
public boolean hasNext() {return true;}
}
return new RandomSequence();
}
- 类隐藏在方法范围内,方法外部它永远不可访问。
- 局部类的方法可以访问来自闭合作用域的变量,就像 lambda 表达式的变量。
private static Random generator = new Random();
public static IntSequence randomInts(int low,int high){
//匿名类
return new IntSequence{
public int next() {return low+generator.nextInt(high - low +1);}
public boolean hasNext() {return true;}
}
}
匿名类定义了该类的接口,实现方法,并构造一个该类的对象。
public static IntSequence randomInts(int low,int high){
//必须提供两个及以上方法时,匿名类才是必须的,不然可以直接用lambda表达式
return () -> low + generattor.necrInt(high - low + 1);
}
第四章 继承和反射
实例变量和静态变量统称为域。类中的域、方法和嵌套类/接口统称为类成员。
一、继承
1、将一个子类方法赋值给父类是合法的,此时调用方法,虚拟机会查看对象的实际类型,并定位方法的版本,这个过程被称作 动态方法查找。
父类赋值同样适用于数组,但可能导致类型错误。
Son[] sons = new Son[10];
Far[] fars = sons;
fars[0] = new Far();
这在编译时是合法的,但在运行时会错误。sons 与 fars 以用相同的 Son[] 数组,它不能持有更上层父亲 Far 的对象。
2、final 修饰方法时,子类不能覆盖方法,修饰类时,该类不能被继承。
3、super代替对象引用同样有效。super::instanceMethod.
二、Object:终极父类
Java 中的所有类都直接或间接的继承与 Object 类,一个类没有显式声明父类时,会隐式的继承Object。
1、toString 方法
Objetc 类默认打印类名与哈希码,一个对象与字符串连接时,Java 编译器会自动调用 toString。
2、equals 方法
equals 默认是判断两个引用是否相等。
无论何时覆盖 equals 方法,必须同时提供一个兼容的 hashCode 方法。
//一个 equals 方法实例,当物品描述和价格均相等时,认为是相等的。
public class Item {
private String description;
private double price;
......
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Item item = (Item) o;
return Objects.equals(description,item.description) && price == item.price;
}
@Override
public int hashCode() {
......
}
}
getClass() 与 instanceof,用 getClass(),因为其中一个类可能是另一个类的子类。而且调用 equals() 的要求之一它是对称的。对于非空对象 x 和 y,x.equals(y) 和 y.equals(x) 需要相同的返回值。
如果是用 instanceof 判断的话,fa.equals(son) 可能会返回 true,但 son.equals(fa) 却会一定返回 false。
只有一种情况使用 instanceof 检查是行得通的:如果相等性以及在父类中固定。就可以使用 instanceof 方法,并将 equals 方法声明为 final。
3、hashCode方法
hashCode 和 equals 方法必须是兼容的。如果 x.equals(y) 为 true,那么 x.hashCode() == y.hashCode()。
@Override
public int hashCode() {
return Objects.hash(description,price);
}
//如果有数组的话,首先使用Array.hashCode()计算一个哈希码,然后再传给Object.hash().
在一个接口中绝不能使重新定义的 Object 类方法成为默认方法。特别是,接口绝不能为 toString、equals、hasCode 定义默认方法。由于类比接口优先,所以这样的方法会被一直使用。
4、克隆对象
Object.clone() 是浅拷贝,只能简单拷贝所有实例变量,如果实例变量都是基本类型或者不可改变,那没问题。但如果它们不是,那么原对象和克隆对象将共享可变的状态。
简单来说就是 String,int 之类的没问题,但 List,Set 这种集合克隆就比较复杂了,都有一些不同的深拷贝方法。
Cloneable 接口:是个没有任何方法的接口,称作标签接口。Object.clone 方法执行浅拷贝前会检查这个接口是否被实现,如果没有会抛出 cloneNotSupperedException。
5、枚举类
枚举常量的构造次序在静态成员之前,所以不能在枚举构造函数中引用任何静态成员。解决方法是在一个静态初始化块中初始化。
三、运行时类型信息和资源
1、Class 类
Object obj = ...;
Class<?> cl = obj.getClass();
Class<?> cl2 = Class.forName(className);
//获取类对象有3种方式
Class<?> cl = Class.forName
Class<?> cl = ClassName.class
Class<?> cl = new Class().getClass()
Class 类有许多有用的方法:jdk8文档在线
比如资源加载、类加载器、服务加载等,在配置一些框架的时候较为常用。
2、反射
反射机制允许程序在运行时检查任意对象的内容,并调用它们的任意方法。
反射先得到 Class 类对象,然后再用类对象的方法去创建对象、访问属性、调用方法。
Spring 的 依赖注入,反转控制 就是用了反射机制。
下边是个简单的模仿 Spring 原理,spring.txt 就可以看作 xml 中读取的信息注入对象,然后 Test.java 中根据文件读取的内容实例化类,调用方法。
//spring.txt
class=reflection.Service1
method=doService1
//Test.java
public class Test {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) throws Exception {
//从spring.txt中获取类名称和方法名称
File springConfigFile = new File("e:\\project\\j2se\\src\\spring.txt");
Properties springConfig= new Properties();
springConfig.load(new FileInputStream(springConfigFile));
String className = (String) springConfig.get("class");
String methodName = (String) springConfig.get("method");
//根据类名称获取类对象
Class clazz = Class.forName(className);
//根据方法名称,获取方法对象
Method m = clazz.getMethod(methodName);
//获取构造器
Constructor c = clazz.getConstructor();
//根据构造器,实例化出对象
Object service = c.newInstance();
//调用对象的指定方法
m.invoke(service);
}
}
3、Proxy代理类:
代理可以在运行时创建实现了给定接口或者接口集的新类,只有当你在编译时还不知道实现哪个接口时,才需要代理。
Spring 框架中 Bean 的作作用域 Session 与 Request 的原理就是代理。
创建一个代理对象时,使用 Proxy 类的 newProxyInstance 方法,该方法需要三个参数:一个类加载器(null为默认)、一个 Class 对象数组,每个被实现的接口都有一个 Class 对象、调用处理器。
**调用处理器:**一个实现了 InvocationHandler 接口的类对象,这个接口只有一个方法:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
一个代理的示例:将一个数组初始化为 Integer 对象的代理。
public static void main(String[] args) {
Object[] values = new Object[1000];
//此处对 Integer 的接口进行了代理,代理了接口所有的方法,具体在编译时才知道实现的哪个接口。
for (int i = 0; i < values.length; i++) {
Object value = new Integer(i);
values[i] = Proxy.newProxyInstance(
null, value.getClass().getInterfaces(),
//此处是InvocationHandler接口的lambd的匿名类实现
(Object proxy, Method m, Object[] margs) -> {
System.out.println(value + "." + m.getName() + Arrays.toString(margs));
return m.invoke(value, margs);
});
}
//二分查找时,才知道是 Integer 实现类。
int position = Arrays.binarySearch(values, new Integer(500));
System.out.println(values[position]);
}
即使不在代码中显式调用 CompareTo 方法,通过代理还是会调用,Integer 的任何接口的所有方法都是被代理的。