java:接口、lambda表达式与内部类

  • Post author:
  • Post category:java

接口(interface)

  1. 接口用来描述类应该做什么,而不指定他们具体应该如何做。接口不是类,而是对符合这个接口的类的一组需求。
  2. 接口定义的关键词是interface
public interface Comparable
{
int compareTo(Object other);
}
  1. 接口是一个完全抽象的类,不会提供任何方法的实现,只会进行方法的定义。
  2. 接口中只能使用两种访问修饰符,一种是public,一种是缺省(包访问权限接口可以被其他类实现,实现接口的类需要提供接口的方法实现,一个接口可以有多个实现。)
  3. 接口中不能有实例字段变量,但是可以包含常量。换句话说,接口中的所有字段都是public static final的。
  4. 实现接口使用关键词implements
  5. 接口不能被实例化,所以接口中没有任何构造方法,试图用new运算符实例化一个接口是错误的,但是却可以声明接口的变量,用接口的实现类来实例化
Comparable x=new Comparable()//错误
Comparable x=new Employee(...);//正确
class Employee implements Comparable
{
public int compareTo(Object otherObject)
{
	Employee other=(Employee) otherObject;
	return Double.compare(salary,other.salary);
}
}
  1. 使用instanceof检查一个对象是否实现了一个接口,同样的类似于类的继承,接口也可以实现继承。每个子类只能有一个超类,但是每个类却可以实现多个接口
class Employee implements Cloneable,Comparable

Employee同时实现Cloneable接口与Comparable接口。

  1. 接口的另一种思路:将方法声明放在抽象类中(抽象类的抽象方法本来就不实现),然后在子类中实现这些方法
    这种思路的缺点在于:java不允许多重继承,一个类只能继承一个抽象类,也就是说一个类只能实现一个接口也,显然这不太好。
  2. 静态方法,只能通过接口名调用,不可以通过实现类的类名或者实现类的对象调用。default方法,只能通过接口实现类的对象来调用,当然如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。
  3. 接口的静态方法
    具体代码实现如下:
public interface Path
{
	public static Path of(URI uri){...}
	public static Path of(String first,String... more){...}
	...
}
  1. 接口的默认方法
    为接口提供一个默认实现,使用default关键词
public interface Comparable<T>
{
	default int compareTo(T other)
	{
		return 0;
	}
}

默认方法带来的好处,如同下面的代码而言,不需要操心remove()或者isEmpty()的实现问题。

public interface Iterator<E>
{
	boolean hasNext();
	E next();
	deafult void remove()
	{
	...;
	}
}

默认方法可以调用其他方法

public interface Collection
{
	int size();// 一个抽象方法
	default boolean isEmpty()
	{
		return size()==0;
	}
}

默认方法可以实现接口演化:已经存在一个接口j,并且有一个类x实现了这个接口j,现在由于某些原因,在接口中又声明一个方法,但是不是一个默认方法,因为类x中没有这个方法,所以对类x的编辑会出错。
默认方法带来的问题
两个接口中同名方法带来的冲突
一个类既继承于一个接口,又继承于一个超类,且超类中有与接口中相同的方法,那么超类中的方法会覆盖掉接口的方法

public interface Person
{
default String getName()
{
return "";
}
}
public interface Named
{
default String getName()
{
return getClass().getName()+"_"+hashCode();
}
class Student implements Person,Named
{
	public String getName()
	{
	//为什么这里不用this,用super,Person只是一个接口
		return Person.super.getName();
	}
}

comparator接口

String类已经实现了Comparable,但是String.compareTo方法按照字典顺序比较字符串,但现在我希望通过字符串长度去比较。
需要重新定义一个实现Comparator的类(一开始学的时候,总认为comparablecomparator是一个,原来是两个不同的接口,而comparator接口的功能更为强大)

在我现在的认识里,以及结合博客来看,Comparable接口与Comprator接口的最大差别在于Comparable是内部接口,Comparator是外部接口

//在真正写代码的时候,下面的接口定义是不用写的
//因为Comparator已经定义在java.util包中了
//我们只需要导入java。util.Comparator
public interface Comparator<T>
{
	int compare(T first,T second);
}
class LengthComparator implements Comparator<String>
{
	public int compare(String first,String second)
	{
		return first.length()-second.length();
	}
}
//具体使用时
String[] friends={"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());

综上所述,排序接口的实现方式有两种
方法一、使用Comparable接口:
让待排序对象所在的类实现Comparable接口,并重写Comparable接口中的compareTo() 。方法缺点是只能按照一种规则排序。

方法二、使用Comparator接口 (推荐使用)
如果一个类要实现java.util.Comparator接口:它一定要实现
int compare(T o1, T o2) 函数,而另一个可以不实现(boolean equals(Object obj)。
使用编写排序方式类实现Comparator接口,并重写新Comparator接口中的compare()方法。优点:想用什么方式排就用什么方式排。

对象克隆

讨论Cloneable接口,我们先来看这一段代码

var original=new Employee("John Public",50000);
Employee copy=original;
copy.raiseSalary(10);

上面的的copy是original变量的一个副本,两者引用的是一个对象,任何对copy的修改都会造成original对象的改变。如果想要copy是一个与original初始状态相同的新的对象,就可以使用clone方法

Employee copy=original.clone();
copy.raiseSalary();

这样copy改变了,但是original却没有改变。但是这样的克隆只是“浅拷贝“,Object类在实现clone方法的时候,只能逐个字段的进行拷贝,但是如果遇到了对象包含子对象的引用,拷贝字段仍然得到的是相同子对象的一个引用。
如果想要实现clone,这个类必须实现Cloneable接口,重新定义clone方法,并指定public访问修饰符(因为所有类的父类Object中的clone方法虽然为protected的,但是子类只能调用受保护的clone方法克隆其自己的对象,如果想在类中随意的使用,必须将其声明为public)
对于一个类的实现者而言,需要有以下的思考:

  1. 默认的clone方法是否满足要求
  2. 是否可以在可变的子对象上调用clone来完成深拷贝

浅拷贝的例子

class Employee implements Cloneable
{
	public Employee clone() throws CloneNotSupportedException
	{
		return (Employee) super.clone();
	}
	....
}

深拷贝的例子

class Employee implements Cloneable
{
	public Employee clone() throws CloneNotSupportedException
	{
		Employee cloned=(Employee) super.clone();

		cloned.hireDay=(Date)hireDay.clone();
		return cloned;
	}
	....
}

lambda表达式

纯粹是为了编写代码时的简单高效
lambda表达式允许你通过表达式来代替功能接口。lambda表达式就和方法一样,提供正常的参数列表和使用这些参数的主体(body,可以是一个表达式或代码块。
函数的方法的要素是:返回值、方法名、参数列表、方法体。
lambda表达式的要素:参数列表 方法体
定义:lambda表达式是一个可以传递的代码块,可以在以后执行一次或多次。
特点:将代码传递到某个对象。这个代码块会在将来的某个时间调用
lambda表达式的基本结构:(parameters) -> expression或者(parameters)->{statements;}
组成部分:
1.parameters:类似方法中的形参列表,这里的参数是函数式接口里的参数,这里的参数类型可以明确的声明 也可不声明而由JVM隐含的推断。另外当只有一个参数时可以省略掉圆括号
语法要点:
* 参数类型可以省略,如果需要省略,每个参数的类型都要省略。
* 参数的小括号里面只有一个参数,那么小括号可以省略
* 如果方法体当中只有一句代码,那么大括号可以省略
* 如果方法体中只有一条语句,且是return语句,那么大括号可以省略,且去掉return关键字

String first,String second)
	->first.length()-second.length()
x->2*x
Comparator<String> comp=(first,second) ->first.length()-second.length()

写成方法形式(代码块之外要加大括号)

(String first,String second)->
{
	if(first.length()<second.length()) return -1;
	else if(first.length()>second.length()) return 1;
	else return 0;
}

无参数形式

() ->{for(int i=100;i>=0;i--) System.out.println(i);}
()->2

忽略参数类型形式

函数式接口

java中有很多封装代码块的接口,并不是所有的接口都是函数式接口,只有当一个接口有且只有一个抽象方法(什么叫抽象方法啊:只定义方法声明,而不定义方法体的方法)时才叫做函数式接口。

定义方式

@FunctionalInterface
public interface Comparator{
int compare(Object o1,Object o2)
} 

@FunctionalInterface
public interface test{
void test();
default void test2(){
	System.out.println("我是一个默认方法");
}
}

lambda表达式与这些接口是兼容的,对于只有一个抽象方法的接口,需要这种接口的对象(往往就是=的右边,需要我们new 一个对象的时候)时,就可以提供一个lambda表达式,这种接口称为函数式接口。
Arrays.sort方法在底层会接受实现了Comparator的某个类的对象。

Arrays.sort(words,(first,second)->first.length()-second.length());

方法引用

  • 方法引用是lambda表达式的一种特殊类型,通过现有方法来创建简单的lambda表达式。具体的下个定义就是说:方法引用是一个更加紧凑,易读的 Lambda 表达式,注意方法引用是一个 Lambda 表达式,其中方法引用的操作符是双冒号 “::”。
  • 本质上,方法引用对于我们如果想改写某些已有的方法是没有帮助的
  • 只有当lambda表达式只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
	//做了两个操作,算长度,作比较
	//所以不能使用方法引用
	s->s.length()==0

方法引用与普通的lambda表达式可以相互转换

//下面的s其实如果在不知道具体的参数类型的时候,我们也没必要知道,但是这个情况可以从前面的推断得出
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer=System.out::println;

//下面这种情况,我们几乎不得而知s是什么类型的
    walker.walk(x->
    {
       x.forEach(s->{System.out.println(s);});
       return null;
    }
    );
    walker.forEach(s->{System.out.println(s);});
    walker.forEach(System.out::println);
有几种方式:
  • 对象引用::实例方法名

  • 类名::静态方法名

  • 类名::实例方法名

  • 类名::new

  • 类型[]::new

构造器引用

  • 构造器引用即为上面的类名::new类型[]::new
    Java有一个限制,无法构造泛型类型T的数组。数组构造器(类型[]::new)会帮助消除这个限制。

在方法引用中还区分绑定的方法引用与未绑定的方法引用。
变量作用域

  • 由于变量作用域的原因,lambda表达式可以捕获作用域中变量的值。这个变量一般就叫做自由变量。
  • 代码块以及自由变量值的统称为:闭包(closure)
  • 来看一段代码,其中text就是自由变量,这个变量并不在lambda表达式中,而是在外围的方法中。
public static void repeatMessage(String text,int delay)
{
	ActionListener listener=event->
	{
		System.out.println(text);
		Toolkit.getDefaultToolKit().beep();
	};
	new Timer(delay,listener).start();
}
  • lambda表达式只能引用值不会改变的自由变量。
  • lambda表达式中不能有同名的局部变量。
    int e1=0;
    Arrays.sort(envelopes,(e1,e2)->
    {
            if(e1[0]!=e2[0])
            {
                return e1[0]-e2[0];
            }
            else{
                return e2[1]-e1[1];
            }
    });
  • lambda中的this关键词指的是外围class的this对象。
    处理lambda表达式
  • 使用lambda表达式的重点在于延迟执行。具体的使用场景如下:
    在一个单独的线程中运行代码
    多次运行代码
    在算法的适当位置运行代码(排序中的比较操作)
    发生某种情况时执行代码(点击了一个按钮,数据到达等等)

Comparator与lambda表达式

内部类

  • 定义:内部类是定义在另一类中的类。
  • 注意:内部类不同于一个文件中多个类的其他类,必须是定义在类中的类(这里有一个小插曲:java一个文件中允许存在多个类,但是只有一个类被声明为public,编译器在编译的时候会分别生成多个类分别的字节码)
  • 为什么要用内部类:内部类可以对一个包中的其他类隐藏
  • 使用内部类访问对象状态:一个内部类既可以访问本类的数据字段,也可以访问创建它的外围类对象的数据字段。
  • 内部类对象是由外部类的方法构造的

为什么内部类能直接使用外部类的实例字段呢?因为内部类的对象总有一个指向外部类对象的隐式引用。
外围类引用:OuterClass.this表示外围类引用。
内部类对象的构造函数outerObject.new InnerClass(construction parameters)
在外部类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
编译器会将内部类转换为常规的类文件,用$区分外部类名与内部类名。

  • 局部内部类
    如果发现某个内部类只在一个方法中只出现了一次,那么可以在相应部位中的方法中创建这个内部类
    声明局部类不能有访问说明符(public、private)
    比如一个内部类就只在一个方法中声明使用,那么可以直接把类的声明全部放入该方法中即可。
  • 匿名内部类
new SuperType(construction parameters)
{
	inner class methods and data
}

匿名内部类不允许有构造器,匿名内部类常常用于实现继承与接口实现过程中方法的重写。
看一段Comparator接口重写的代码

Arrays.sort(envelopes,new Comparator<int[]>()
{
   public int compare(int []e1,int []e2){
       if(e1[0]!=e2[0])
       {
           return e1[0]-e2[0];
       }
       else{
           return e2[1]-e1[1];
       }
   }
});

继承Thread类并实现其中的run方法

   new Thread(){
       @Override
       public void run() {
           for (int i = 0; i < 100; i++) {
               if(i % 2 == 0){
       System.out.println(Thread.currentThread().getName() + ":" + i);
               }
           }
       }
   }.start();
  • 静态内部类
    如果只是希望将类隐藏再类的内部,而在内部类中不生成外部类的引用,或者并没有使用到外部类的实例字段。
    可以将内部类声明为static类,也只有内部类才能被声明为static。
    具体的讲解内部类的文章,可以查看我的博客中的另一篇。

版权声明:本文为cillian_bao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。