本博客只用于个人查漏补缺
接口
接口的概念
- 接口中所有方法自动为 public 方法,定义接口时,不必提供关键字 public
- 接口可以定义常量
- 接口没有实例字段
- JAVA8之前,接口中不能实现方法,之后可以
- 实现接口时,必须把方法默认声明为 public ,否则编译器报错
- Comparable接口文档建议 compareTo 方法应当与 equals 方法兼容。
当 x.equals(y) 时 x.compareTo(y) 就应当返回 0
Java API 大多实现 Comparable 接口的类都遵从了,除了 BigDecimal,跟精度有关系 1.00 和 1.0 - Arrays.sort()
要求数组中的元素必须属于实现了 Comparable 接口的类,且元素间必须可比较
接口的属性
- 可以像使用 instanceof 检查一个对象是否属于某个特定类一样
也可以使用 instanceof 检查一个对象是否实现了某个特定的接口if(object1 instanceof Comparable) {...}
- 与接口中的方法自动被设置成 public 一样,接口中的字段总是 public static final
- 每个类只能有一个超类,但是可以实现多个接口,使用逗号将要实现的各个接口分隔开
静态与私有方法
- 在 Java 9 中,接口中的方法可以使 private,这个方法可以是 静态方法 或者 实例方法
由于私有方法只能在接口本身的方法中使用,所以他只能用于接口中其他方法的辅助方法
默认方法
- 可以为接口方法提供一个默认实现,必须用 default 修饰符标记这个方法
default void remove() {throw new UnsupportedOperationException("remove")}
- 默认方法可以调用其他方法
- 作用1,迭代器中的remove方法,实现迭代器需要提供 hasNext 和 next 方法
这些方法没有默认实现,它们依赖于遍历的数据结构 - 作用2是“接口演化”,保证源代码兼容,默认方法可以在为以前的接口增加方法时候使用,这样以前继承了这个接口的类,就不需要修改,因为新加入的方法是 默认方法,自动有默认实现
- 如果在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法
在Java中,
超类优先(如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略)
接口冲突(如果一个接口提供了一个默认方法,另一个接口也提供了一个同名参数相同的方法,必须覆盖这个方法来解决冲突) - 如果一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法
遵循,类优先,即只会考虑类方法
对象克隆
- 浅拷贝:默认的克隆操作,只拷贝基本字段,不拷贝克隆对象中引用的其他对象
如果克隆对象的子对象是不可变的,或者子对象没有更改器方法,那么就是安全的 - 深拷贝:重新定义 clone 方法,克隆所有子对象
- Cloneable 接口出现和接口的使用没有关系,因为 clone 方法是Object类继承而来的
Cloneable接口是 标记接口,不含任何方法,唯一的作用就是允许在类型查询中视同 instanceof - 所有数组类型都有一个公共的 clone 方法,不是受保护的,可以用这个方法建立一个新数组,包含原数组所有元素的副本
int[] arr = {1,2,3,4,5};
int[] arr2 = arr.clone();
lambda表达式
将代码块传递到某个对象,这个对象将会在某个时间调用(延迟执行)
语法
- 即使lambda表达式没有参数,仍然要提供空括号,类似于无参方法
() -> {....}
- 如果可以推导出一个lambda表的参数类型,则可以忽略其类型
(s1, s2) -> {...}
- 如果方法只有一个参数,而且这个参数类型可以推导出,那么可以省略小括号
a -> {....}
- 如果一个lambda表达式只在一些分支返回值,这是不合法的
如(int x) -> {if (x > 0) return 1;}
函数式接口
- 只有一个抽象方法的接口,可以提供一个lambda表达式
- 在Java中,对lambda表达式所能做的也就是转换为 函数式接口
- ArrayList类有一个 removeIf 方法,它的参数就是一个 Predicate,这个接口用来传递 lambda 表达式,如,下面的语句将从一个数组列表删除所有null值
list.removeIf(e -> e == null)
- 供应者(supplier)没有参数,调用时会生成一个T类型的值,用于实现懒计算
LocalDate hire = Objects.requireNunNullOrElseGet(day, new LocalDate(1970,1,1));
这种情况下,每次都会构建默认的LocalDate,而day 为null的情况很少,因此,可用通过supplier实现延迟这个计算LocalDate hire = Objects.requireNunNullOrElseGet(day, () -> new LocalDate(1970,1,1));
此时只有在需要值时,才会调用供应者
方法引用
System.out::println;
- 指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法
- 类似于lambda表达式,方法引用也不是一个对象
- 要用
::
运算符分隔方法名 与 对象 或 类名
主要有三种情况:对象::实例方法
lambda参数作为方法的显示参数传入类::实例方法
String::trim ,lambda表达式会成为隐式对象类::静态方法
Integer::valueOf,lambda表达式会传递到这个静态方法
只有当那个lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写成方法引用 - 方法引用不能独立存在,总是会转换为函数式接口的实例
- 包含对象的方法引用 与 等价的lambda表达式还有一个细微差别,如果对象为空,方法引用会直接抛出异常,而lambda表达式只有在调用时才会抛出异常
- 可以在方法引用中使用 this 、super参数
构造器引用
- 与方法引用类似,不过方法名为new
Person::new
这就是Person构造器的一个引用,具体哪一个构造器呢,取决于上下文 - 数组的构造器引用
Integer[]::new
变量作用域
- lambda表达式可以“捕获”外围作用域中变量的值,只要确保所捕获的值时明确定义的
即lambda表达式中,只能引用值不会改变的外部变量
lambda表达式中捕获的变量必须是 final 变量 - lambda表达式的与 嵌套块 有相同的作用域,在lambda表达式中声明一个与局部变量同名的参数或局部变量是不合法的
- lambda表达式中的 this 含义与外面一致
内部类
使用原因:
内部类可以对同一个包中的其他类隐藏
内部类方法可以访问定义这个类的作用域中的数据,包括私有数据
使用内部类访问对象状态
- 一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围对象的数据字段
所以,内部类的对象总有一个 隐式引用 ,指向创建它的外部类对象 - 外围类的引用在构造器中设置,编译器会修改所有内部类的构造器,添加提个对应的外围类引用的参数
- 只有内部类是可以是 private ,而常规类只可以有 包可见性(default、protected) 或 公共可见性(public)
内部类的特殊语法规则
- 外围类引用:
OuterClass.this
如:Person.this - 内部类对象的构造器:
outerObject.new InnerClass(construction param)
- 外围类的作用域之外引用内部类:
OuterClass.InnerClass
- 内部类中声明的所有静态字段都必须是 final,并初始化为一个编译时常量
- 内部类不能有static方法,除了 访问外围类的静态字段和方法 的静态方法
内部类与编译器
- 内部类是一个编译器现象,与虚拟机无关,编译器会讲内部类转换为常规的类文件,用
$
分隔外部类名和内部类名
局部内部类
当这个类的对象只被一个方法创建一次时,在一个方法中的局部地定义这个类
- 声明局部类时,不能有访问说明符(private,public) 局部类的作用域被限定在声明这个局部类的块中
- 局部类的优势,对外部世界完全隐藏,连外围类中的其他代码都不能访问它
由外部方法访问变量
- 与其他内部类相比较,局部类可以访问 方法中的局部变量
不过,这些变量必须是 final 变量
匿名内部类
只创建这个类的一个对象,不需要为类指定名字
- 语法:
new superType(construcrion param) {...}
superType可以是接口,内部类要实现这个接口,也可以是一个类,内部类就要扩展这个类 - 由于构造器名字必须与类名相同,而匿名内部类没有类名,所以匿名内部类不能有构造器,实际上,构造参数要传递给超类的构造器。但,只要内部类实现一个接口,就不能有任何构造参数,不过仍然要小括号
尽管匿名内部类不能有构造器,但可以提供一个对象初始化块new Person("asd") { {init} ....}
- 如果构造参数列表的结束小括号后面跟一个开始大括号,就是在定义匿名内部类
- 静态方法中要得到当前类名,不能使用 this,静态方法没有this,可以使用如下技巧
new Object(){}.getClass().getEnclosingClass()
上式,创建了一个匿名内部类,getEnclosingClass得到他的外围类
双括号初始化
利用了内部类语法,使用匿名列表
ArrayList<Integer> array = new ArrayList<>(){{
add(1);
add(2);
add(3);
}};
System.out.println(array.toString());
外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象初始化块
通过new得到这个ArrayList的子类的实例并向上转型为ArrayList的引用
适用于这些数组列表不需要再使用的情况,但有可能会造成内存泄露
Map,Deque,Set等集合也有类似技巧
静态内部类
static声明的类
- 静态内部类类似于其他内部类,不过静态内部类的独享没有生成它外围类对象的引用
- 只要内部类不需要访问外围类对象,就应该使用静态内部类
- 与常规内部类不同,静态内部类可以由静态字段和方法
- 在接口中声明的内部类自动是 public 和 static
代理类
创建代理对象
使用Proxy类的newProxyInstance方法,有三个参数
一个类加载器
一个Class对象数组
一个调用处理器
Student s1 = new Student();
//动态代理增强s1对象
/*
三个参数:
类加载器:真实对象.getClass().getClassloader()
接口数组:真实对象.getClass().getInterfaces()
处理器:new InvocationHandler()
*/
Student proxy_s1 = (Student)Proxy.newProxyInstance(s1.getClass().getClassLoader(), s1.getClass().getInterfaces(), new InvocationHandler() {
//代理逻辑编写的方法,代理对象调用的所有方法都会触发该方法的执行
/*
参数:proxy:代理对象
method:代理对象调用的方法,被封装为对象
args:代理对象调用实际方法时,传递的实际参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("每次都会调用我");
if(method.getName().equals("setName")){//如果调用setName()方法就增强,否则不增强
String name = args[0];
name = "李四";
String obj = (String) method.invoke(s1,name);//使用真实对象调用该方法,改变参数调用,同时改变返回值为String
return obj+"和王五";
}else{
Object obj = method.invoke(s1,args);//使用真实对象调用该方法,原样调用
return obj;
}
}
});
System.out.println(proxy_s1.setName("张三");)//输出“每次都会调用我”,“李四和王五”,
//证明每次调用代理对象的真实对象的方法都会执行invoke()
//并且method对象是代理对象调用的方法的被封装后的对象,args是传递的参数
代理类的特性
- 代理类是在程序运行过程中动态创建的,一旦被创建,就成为了常规类
- 所有代理类都扩展Proxy类,一个代理类只有一个实例字段,调用处理器,在Proxy超类中定义,完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中
- 代理类总是 public 或 final,如果代理类实现的所有接口都是 public 这个代理类就不输入任何特定的包,斗则所有非公共的接口都必须属于同一个包,代理类也属于这个包