Java基础知识扫盲(二)——接口、lambda 表达式、内部类
接口
接口不是类,是对类的一组需求描述,类要遵从接口描述的统一格式进行定义。
- 接口中所有方法都是public,不需要声明关键字public
- 接口不能包含有实例域和静态方法
- SE 8之后可以在接口中实现默认方法,用default修饰符标识
- 可以把接口看成是没有实例域的抽象类
- 可以声明接口变量,不可以实例化接口
- instanceOf也可以检测一个对象是否实现了某个特定的接口
- 接口中的域将被自动设为 public static final
接口中为什么新增default 方法?
- 已经存在的大多数实现体,会因为接口中新增的方法,而无法编译,但是default可以避免这些问题,保证源代码兼容
- 实现接口时更专注于覆盖真正关心的方法。
解决默认方法冲突
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。 如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型 (不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突
class Student implements Person, Named{
...
}
Person和Named都提供了一个getName方法,Java编译器会报告一个错误,让程序员解决这个二义性。
当然,如果两个接口都没有为共享方法提供默认实现, 那么就与 Java SE 8之前的情况一样,这里不存在冲突。 实现类可以有两个选择: 实现这个方法, 或者干脆不实现。如果是后一种情况, 这个类本身就是抽象的。
- 一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。
只会考虑超类方法, 接口的所有默认方法都会被忽略。
tips:千万不要让一个默认方法重新定义 Object 类中的某个方法。
例如, 不能为 toString或 equals 定义默认方法, 尽管对于 List 之类的接口这可能很有吸引力, 由于“ 类优先”规则, 这样的方法绝对无法超越 Object.toString 或 Objects.equals
回调
可以指出某个特定事件发生时应该采取的动作。例如定时任务:
public class CallBackMeth{
public static void main(String[] args) {
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();
JOptionPane.showMessageDialog(null,"Quit?");
System.exit(0);
}
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep(); // 响铃
}
}
Comparator 比较器
自定义字符串长度比较器:
public class StringLengthComparator {
public static void main(String[] args) {
Comparator<String> comparator = new LengthComparator();
System.out.println(comparator.compare("hello", "world"));
System.out.println("hello".compareTo("world"));
String[] names = { "songyanyan", "songjunfeng", "syy", "jeff" };
Arrays.sort(names, comparator);
System.out.println(names);
}
}
class LengthComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
Cloneable 克隆实现
// clone方法是Object的一个protected方法,不可以直接调用
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
对象拷贝和克隆.png
默认的克隆操作是“浅克隆”。如果原对象和浅克隆对象共享的子对象是不可变的,这种共享即是安全的。例如String,或者在对象的生命周期中没有更改器方法会改变它,也没有方法会生成它的引用。但是Date是可变的,例如下面的浅拷贝示例:
浅拷贝.png而通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆出所有子对象。
- 实现 Cloneable 接口
- 重新定义 clone 方法,并指定 public 访问修饰符
- 必须当心子类的克隆。
@Override
public Employee clone() throws CloneNotSupportedException {
// call Object.clone()
Employee cloned = (Employee) super.clone();
// clone mutable fields 克隆可变域
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
所有数组类型都有一个 public 的 clone 方法, 而不是 protected: 可以用这个方法建立一个新数组, 包含原数组所有元素的副本,只适用于(一维)数组。
需要明确的是Java中不存在多维数组的说法,二维数组即为数组的数组
int[][] arrayTestCloned2 = new int[arrayTest.length][];
for (int i = 0; i< arrayTest.length; i++){
arrayTestCloned2[i] = Arrays.copyOf(arrayTest[1],arrayTest[1].length);
}
lambda表达式
// 比较器
Comparator<String> comparator = (first, second) -> {
return first.length() - second.length();
};
// 事件监听
ActionListener listener = e -> System.out.println("the time is " + new Date());
Timer timer = new Timer(1000, e -> System.out.println());
函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。这种接口称为函数式接口 (functional interface )
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
...
}
方法引用
Timer timer1 = new Timer(1000, System.out::println);
List<String> list = new ArrayList();
list.sort(String::compareToIgnoreCase);
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
- super::instanceMethod
构造器引用
List<Integer> nums = Arrays.asList(1, 5, 4);
Stream<Apple> appleStream = nums.stream().map(Apple::new);
Apple[] apples = appleStream.toArray(Apple[]::new);
编译器会选择有一个 Integer 参数的构造器, 因为它从上下文推导出这是在对一个整型参数调用构造器。
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
变量作用域
public static void repeatMessage(String text, int delay) {
ActionListener listener = event ->{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
text是repeatMessage的参数而不是lambda表达式的。lambda表达式有3个部分:
- 代码块
- 参数
- 自由变量的值(非参数并且不在代码块儿中定义的变量)
这个text就是lambda表达式自由变量。表示lambda表达式的数据结构一定有存储自由变量的值。
在 lambda 表达式中, 只能引用值不会改变的变量。如果在 lambda 表达式中改变变量, 并发执行多个动作时就会不安全。变量在外部改变,也是不合法的.
public static void numDown(int num, int delay) {
for(int i=0;i<= num;i++){
ActionListener listener = event ->{
System.out.println(num--); //Error: 不能引用可变的变量值num
System.out.println(i); //Error: 不可以调用可变变量i
};
new Timer(delay, listener).start();
}
}
IDE相关提示.png
即lambda表达式中扑火的变量必须/实际是最终变量(这个变量初始化之后就不会再为它赋新值)。
不能声明一个局部变量同名的参数/局部变量。
String first = "Hello";
Comparator<String> stringComparator = (first, second) -> first.length() - second.length(); //Error:在域中已经声明过变量first
在lambda表达式中使用this。会调用Application相应方法。lambda表达式的作用域嵌套在init方法中。lambda表达式中this的含义并没有变化。
处理lambdab表达式
重点:延迟执行。
public static void repeat(int n, IntConsumer consumer) {
for (int i = 0; i < n; i++)
consumer.accept(i);
}
调用:
repeat(10, i -> System.out.println(i));
最好使用这些特殊化规范来减少自动装箱。
基本类型的函数式接口.png 常用的函数式接口.png大多数标准函数式接口都提供了非抽象方法来生成或合并函数。
如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。
Comparator
内部类
- 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷
- 内部类对象拥有一个对外围类对象的引用
- 外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数(为这个内部类生成一个默认的构造器)。
- 在方法调用内部类之后,会创建内部类的对象,编译器会将this引用传递给当前的内部类构造器
特殊语法规则
public class LocalInnerClassTest{
public static void main(String[] args)
{
TalkingClock clock = new TalkingClock();
clock.start(1000, true);
// keep program running until user selects "Ok"
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TalkingClock
{
/**
* Starts the clock.
* @param interval the interval between messages (in milliseconds)
* @param beep true if the clock should beep
*/
public void start(int interval, boolean beep){
class TimePrinter implements ActionListener
{
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();
}
}
-
OuterClass.this 表示外围类引用
-
outerObject.new InnerClass(construction parameters) 明确地编写内部对象的构造器
-
OuterClass.InnerClass 外围类的作用域外,这样引用内部类
-
内部类中声明的所有静态域都必须是 final
-
内部类 static 方法,只能访问外围类的静态域和方法
访问外部非静态域sum报错.png -
编译器将会把内部类翻译成用$美元符号分隔外部类名与内部类名的常规类文件, 而虚拟机则对此一无所知。例如,在 TalkingClock类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$TimePrinter.class
public class TalkingClock$TimePrinter
{
public TalkingClock$TimePrinter(TalkingClock);
public void actionPerformed(java.awt.event.ActionEvent);
final TalkingClock this$0;
}
编译器使内部类为了引用外围类, 生成了一个附加的实例域 this$0
class TalkingClock{
private int interval;
private boolean beep;
public TalkingClock(int, boolean);
static boolean access$0(TalkingClock);
public void start();
}
TalkingClock(外围类)被编译器添加了一个静态方法access$0,它将(返回作为参数)传递给它的对象域beep(外围类的私有域)
if (beep)
if (TalkingClock.access$0(outer))
存在一定的安全问题。如果内部类访问了私有数据域, 就有可能通过附加在外围类所在包中的其他类访问它们。
- 局部类有一个优势,即对外部世界可以完全地隐藏起来。
- 局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部
类的块中。 - 除 start 方法之外,没有任何方法知道 TimePrinter 类的存在
局部类可以访问局部变量(必须是事实上的final类型)
TimePrinter 类在 beep 域释放之前将 beep 域用start 方法的局部变量进行备份
查看TalkingClock$TimePrinter类
class TalkingClock$1TimePrinter{
TalkingClock$1TimePrinter(TalkingClock, boolean);
public void actionPerformed(java.awt.event.ActionEvent);
final boolean val$beep;
final TalkingClock this$0;
}
- 注意构造器的 boolean 参数和 val$beep 实例变量。
- 当创建一个对象的时候, beep 就会被传递给构造器,并存储在 val$beep 域中。
- 编译器必须检测对局部变量的访问, 为每一个变量建立相应的数据域, 并将局部变量拷贝到构造器中, 以便将这些数据域初始化为局部变量的副本
局部类的方法只可以引用定义为 final 的局部变量,所以在编译之后局部变量在局部类中的拷贝为final类型。
匿名内部类
class TalkingClock{
/**
* Starts the clock.
* @param interval the interval between messages (in milliseconds)
* @param beep true if the clock should beep
*/
public void start(int interval, boolean beep)
{
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();
}
}
- 匿名类不能有构造器。取而代之的是,将构造器参数传递给超类 ( superclass) 构造器
Person queen = new Person("Mary");
// a Person object
Person count = new Person("Dracula") { . . . }
// an object of an inner class extending Person
// 如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类
构造一个数组列表:
new ArrayList<String>()
{// 匿名子类
{// 调用外层类ArrayList的方法add 构造对象块
add("Harry");
add("Tony");
}
};
调用 getClass 时调用的是 this.getClass(),而
静态方法没有 this,获取该静态方法的类:
new Object(){}.getCIass().getEnclosingClass() // gets class of static method
new Object(){}会建立 Object 的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类, 也就是包含这个静态方法的类
静态内部类
public class StaticInnerClassTest{
public static void main(String[] args){
double[] d = new double[20];
for (int i = 0; i < d.length; i++)
d[i] = 100 * Math.random();
ArrayAlg.Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
class ArrayAlg{
public static class Pair{
private double first;
private double second;
public Pair(double f, double s){
first = f;
second = s;
}
public double getFirst(){
return first;
}
public double getSecond(){
return second;
}
}
/**
* Computes both the minimum and the maximum of an array
* @param values an array of floating-point numbers
* @return a pair whose first element is the minimum and whose second element
* is the maximum
*/
public static Pair minmax(double[] values){
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values){
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
}
- 在内部类不需要访问外围类对象的时候, 应该使用静态内部类
- 与常规内部类不同,静态内部类可以有静态域和方法
- 声明在接口中的内部类自动成为 static 和 public 类
代理(proxy)
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。具备的方法:
- 指定接口所需要的全部方法
- Object 类中的全部方法, 例如, toString、 equals 等
不能在运行时定义这些方法的新代码。要提供一个调用处理器(invocationhandler)。调用处理器是实现了 InvocationHandler 接口的类对象
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
return ...;
}
}
创建代理对象
需要使用Proxy.newProxyInstance方法,三个参数:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
- 一个类加载器。目前,用null表示使用默认的类加载器
- 一个Class对象数组。每个元素都是需要实现的接口
- 一个调用处理器。
如何定义一个处理器? 能够用结果代理对象做些什么?
- 路由对远程服务器的方法调用
- 在程序运行期间,将用户接口事件与动作关联起来
- 为调试, 跟踪方法调用
使用代理和调用处理器跟踪方法调用。
public class ProxyTest {
public static void main(String[] args) {
Object[] objects = new Object[500];
Class[] interfaces = new Class[] { Comparable.class, Iterable.class };
for (int i = 0; i < objects.length; i++) {
Integer value = i + 1;
ClassTraceHandler handler = new ClassTraceHandler(value);
objects[i] = Proxy.newProxyInstance(null, interfaces, handler);
}
Integer key = new Random().nextInt(objects.length) + 1;
int result = Arrays.binarySearch(objects, key);
if (result >= 0)
System.out.println(objects[result]);
}
}
class ClassTraceHandler implements InvocationHandler {
private Object target;
public ClassTraceHandler(Object t) {
target = t;
}
@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(")");
return method.invoke(target, args);
}
}
结果:
250.compareTo(161)
125.compareTo(161)
187.compareTo(161)
156.compareTo(161)
171.compareTo(161)
163.compareTo(161)
159.compareTo(161)
161.compareTo(161)
161.toString()
161
随机生成一个值,二分找到相应数值。
二分会调用compareTo方法,为代理的Comparable接口中的方法。
System.out.println,会调用对象的toString方法输出到控制台。