一.内部类
内部类可以看做是外部类的一个成员,那么内部类可以使用 public/缺省/protected/private 修饰,还可以是static修饰。
1、为什么使用内部类:
1)增强封装,把内部类隐藏在外部类之内,不许其他类访问内部类。
2)内部类能提高代码的可读性和可维护性,把小型类嵌入到外部类中结构上代码更靠近。
3)内部类可以直接访问外部类的成员。
一般在开发中什么时候使用内部类.
1)这个类只需要让当前的外部类访问.
2)匿名内部类.
2、内部类根据使用的修饰符不同或者位置不同,可分为四种:
对于每个内部类来说,Java 编译器会生成独立的 .class 文件
1)非静态内部类/实例内部类:外部类名$内部类名.class
没有使用static修饰,说明非静态内部类属于外部类的对象,不属于外部类本身。
特点:
(1)创建实例内部类前,必须存在外部类对象,通过外部类对象创建内部类对象(当存在内部类对象时,一定存在外部类对象).
Outter.Inner in = new Outter().new Inner();
(2)实例内部类的实例自动持有外部类的实例的引用,内部类可以直接访问外部类成员(看反编译).
(3)外部类中不能直接访问内部类的成员,必须通过内部类的实例去访问.
(4)实例内部类中不能定义静态成员,只能定义实例成员.
(5)如果实例内部类和外部类存在同名的字段或方法xxx时,那么
在内部类中使用 this.xxx 表示访问内部类成员.
在内部类中使用 外部类对象.xxx 表示外部类成员.
在外部类中使用 this.xxx 表示访问外部类成员.
2)静态内部类: 外部类名$内部类名.class
内部类使用static修饰。
特点:
(1)静态内部类的实例不会自动持有外部类的特定实例的引用,在创建内部类的实例时,不必创建外部类的实例.
Outter.Inner in = new Outter.Inner();
(2)静态内部类可以直接访问外部类的静态成员,如果访问外部类的实例成员,必须通过外部类的实例去访问.
(3)在静态内部类中可以定义静态成员和实例成员.
(4)测试类可以通过完整的类名直接访问静态内部类的静态成员.
3)局部内部类(打死都不用,破坏封装): 外部类名$数字内部类名.class
定义在方法中的内部类,其可见范围是当前方法和局部变量是同一个级别.
局部内部类只能访问 final 修饰的局部变量
特点:
(1)不能使用 public,private,protected,static修饰符.
(2)局部内部类只能在当前方法中使用.
(3)局部内部类和实例内部类一样,不能包含静态成员.
(4)局部内部类和实例内部类,可以访问外部类的所有成员.
(5)局部内部类访问的局部变量必须使用final修饰(在Java8中是自动隐式加上final,但是依然是常量,不能改变值). 原因:
如果方法中的局部变量不使用 final 修饰,存在于方法的内存空间中,
方法调用结束,方法空间就被消化,局部变量也就被销毁了,
使用 final 修饰的变量,表示常量,存放与常量池,方法销毁之后,该常量依然存在,则对象可以继续访问.
4)匿名内部类(Anonymous):外部类名$数字.class
匿名内部类是一个没有名称的局部内部类,适合于仅使用一次的类。
特点:
(1)匿名内部类本身没有构造器,但是会调用父类构造器.
(2)匿名类尽管没有构造器,但是可以在匿名类中提供一段实例初始化代码块,JVM在调用父类构造器后,会执行该段代码.
(3)内部类处理可以继承类之外,还可以实现接口.
二.lambda表达式
简单示例
public class Apple {
private Integer weight = 0;
private String color = "";
public Apple(Integer weight, String color) {
this.weight = weight;
this.color = color;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String toString() {
return "Apple{" +
"color='" + color + '\'' +
", weight=" + weight +
'}';
}
这仅仅是一个普通的实体类。然后我们有一组这样的对象,在实现中需要针对这些Apple对象的weight属性进行排序。这是一个非常简单的问题,一种最传统的方式无非就是实现一个Comparator的接口,再将该实现的对象作为参数传递到原来的sort方法中去。其详细的实现如下:
import java.util.Comparator;
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
主函数代码如下:
import java.util.*;
public class Sorting {
public static void main(String[] args) {
List<Apple> inventory = new ArrayList<>();
inventory.addAll(Arrays.asList(new Apple(80, "green"),
new Apple(155, "green"), new Apple(120, "red")));
inventory.sort(new AppleComparator());
System.out.println(inventory);
}
}
这样,我们就实现了一个基于自定义对象进行排序的功能。它能够实现排序的要点是inventory.sort方法里需要接收的参数是Comparator类型的对象。而这个类型的对象必须要实现compare方法。从功能实现的角度来说,我们的方法需要传递的参数类型和实际传递的都是对象,也正好符合一切皆对象的这个说法。
当然,这种实现方式显得比较繁琐,因为我们这里仅仅是需要实现一个简单的接口,这里却需要定义一个类,专门实现它。而且真正能够在排序里起作用的就是compare这个方法。只有根据它才能知道怎么排序。可是在这里没办法,必须针对这个需要的方法行为包装成一个对象传递过去。
当然,我们还想到一种稍微简单一点的方法,就是使用匿名类,这种实现的方式如下:
import java.util.*;
public class Sorting {
public static void main(String[] args) {
List<Apple> inventory = new ArrayList<>();
inventory.addAll(Arrays.asList(new Apple(80, "green"),
new Apple(155, "green"), new Apple(120, "red")));
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
System.out.println(inventory);
}
}
1.更简洁的方法
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
我们甚至可以将类型信息给省略掉:
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
甚至更简单的情况下可以用如下代码来描述:
inventory.sort(comparing(Apple::getWeight));
在上述代码里,我们没有新建什么对象,而是采用一种类似于方法传递的方式来实现排序的目的。这里,我们使用的就是lambda表达式。在函数式编程语言的概念里,这里相当于将一个函数作为参数传递到另外一个对象方法里。笼统的来说,在java8里新加入的特性是的我们可以将一个函数作为参数来传递了。
那么,该怎么来理解lambda表达式呢?一个lambda表达式可以视为一个匿名方法,但是它可以像普通的对象参数那样被传递。它没有具体定义的名字,但是可以有一组参数,函数体以及返回类型。它甚至可以包含有被抛出的异常列表。我们针对它的每个具体特征来讨论
2.lambda表达式语法
从前面示例我们可以看到一个lambda表达式的样式基本如下:
它不需要定义名字,所以首先是包含有输入的函数参数列表,一般用一个括号来包含,比如:()
然后是一个箭头符号,后面包含具体的函数语句,可以有一句或者多句。比如: a.length - b.length。
所以上述示例的解析结构如下图:
基于上述的描述,我们可以定义很多类似的lambda表达式,它们对应的函数签名如下:
(String s) -> s.length
输入参数为String类型,返回结果为int类型的函数。它的函数签名样式为: (String) -> int
(Apple a) -> a.getWeight() > 150
输入参数为Apple类型,返回结果为boolean类型的函数,函数签名样式为:(Apple) -> boolean
在需要返回结果的函数签名里,如果函数语句只有一句的话,该语句执行的结果将作为函数结果返回。
(int i, int j) -> {
System.out.println(i);
System.out.println(j);
System.out.println(“Result printed”);
}
输入参数为int, int,返回结果为void,即没有返回任何结果。函数签名样式为:(int, int) -> void
(String a, String b) -> {
System.out.println(a);
return a + b;
}
该示例代码的输入参数为(String, String),返回结果为String。但是因为函数中有多条语句,所以需要添加一个return语句在最后作为返回的结果。
看了上述简单的介绍后,估计我们心里还是有很多的疑问。比如说,为什么上述的代码可以对等的替换一个接口呢?难道说它和一个接口是等价的?那么它是不是可以替换所有类型的接口呢?另外一个就是,我们前面写的lambda表达式里,对于传入的参数甚至连类型都没有声明,它是怎么知道我们的参数类型的呢?
3.函数式接口(Functional interfaces)
在前面的示例代码里,我们看到,可以将一个lambda表达式用在一个接口所使用的地方。在java8里,lambda表达式可以传递和识别的类型是函数式接口。那么函数式接口是什么呢?
在java里,我们经常可以看到不少只包含有一个方法定义的接口,比如Runnable, Callable, Comparator等。而这种仅仅包含有一个接口方法的接口就可以称其为函数式接口。需要特别注意的一点就是,这里指的方法是接口里定义的抽象方法。由于java8里引入了默认方法(default method),在接口里也可以定义默认方法的实现。但是这些方法并不算抽象方法。关于默认方法我们会在后续的文章里讨论。
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// details ignored.
}
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
在这里@FunctionalInterface相当于函数式接口的声明,类似于我们继承类里实现某个方法使用的@Override声明。它表示该接口是函数式接口,方便在编译的时候进行检查。
这样,我们可以发现,每个函数对象对应一个函数式接口的实例。所有传递单个方法接口的地方就可以用lambda表达式来替换了。
除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
Predicate——接收T对象并返回boolean
Consumer——接收T对象,不返回值
Function<T, R>——接收T对象,返回R对象
Supplier——提供T对象(例如工厂),不接收值
UnaryOperator——接收T对象,返回T对象
BinaryOperator——接收两个T对象,返回T对象
4.目标类型(Target typing)
在前面的讨论中我们发现,其实一个lambda表达式就是一个对应的函数式接口对象。但是,一个lambda表达式它本身并没有包含它到底实现哪个函数式接口的信息。我们怎么知道我们定义的某个lambda表达式可以用到某个函数式接口呢?实际上,对于lambda表达式的类型是通过它的应用上下文来推导出来的。这个过程我们称之为类型推导(type inference)。那么,在上下文中我们期望获得到的类型则称之为目标类型。该怎么来理解上述的内容呢?
例如,下面代码中的lambda表达式类型是ActionListener:
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
但是同样的lambda表达式在不同的上下文中可以有不同的类型:
Callable<String> c = () -> "done";
PrivilegedAction<String> a = () -> "done";
第一个lambda表达式() -> "done"是Callable的实例,而第二个lambda表达式则是PrivilegedAction的实例。
下面,我们来结合前面的示例代码做一个详细的类型检查分析:
首先,我们这部分应用lambda表达式的代码如下:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
-
我们首先检查inventory.sort方法的签名,它的详细签名如下:void sort(Comparator<? super E> c)。
-
那么它期待的参数类型是Comparator.
-
我们来看Comparator接口,它是一个函数式接口,并有定义的抽象方法compare。
-
这个compare方法的详细签名如下:int compare(Apple o1, Apple o2),这表示这个方法期待两个类型为Apple的输入参数,并返回一个整型的结果。
-
比对lambda表达式的函数签名类型,它也是两个输入类型为Apple,并且输出为int类型。
这样,lambda表达式的目标类型和我们的类型匹配了。
总结起来,当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:
T是一个函数式接口
lambda表达式的参数和T的方法参数在数量和类型上一一对应
lambda表达式的返回值和T的方法返回值相兼容(Compatible)
lambda表达式内所抛出的异常和T的方法throws类型相兼容
由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formal parameter)类型,所以我们没有必要把已知类型再重复一遍。也就是说,lambda表达式的参数类型可以从目标类型中得出:
Comparator comp = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
在上面的例子里,编译器可以推导出a1和a2的类型是Apple。所以它就在lambda表达式里省略了a1, a2的类型声明。这样可以使得我们的代码更加简练。
5.方法引用(Method references)
我们定义一些lambda表达式并传递给一些函数,这种方式可以使得我们实现的代码很简练。但是在有的情况下,我们已经有一些方法实现同样的功能了,那么我们能不能想办法重用这些原有的功能而不至于自己去重复实现呢?
像我们前面代码示例里使用的如下代码:
inventory.sort(comparing(Apple::getWeight));
这里就是引用了一个方法。将它作为一个参数传递给comparing方法。这里的Apple::getWeight可以看做lambda表达式p -> p.getWeight()的一个简写形式。其中Apple::getWeight就是一个对Apple类中实现方法getWeight的引用。所以,我们可以将方法引用当做lambda表达式的语法糖。
方法引用有很多种,它们的语法如下:
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
对于静态方法引用,我们需要在类名和方法名之间加入::分隔符,例如Integer::sum。
对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符:
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
针对上述的讨论,我们先来看个示例。假设我们需要对一组String进行排序,并忽略大小写。那么我们可以采用lambda表达式写成如下:
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
按照我们这里方法引用的定义,我们可以进一步将代码简化成如下:
List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);
实际上对于方法引用的类型检查和lambda表达式的类型检查过程基本上一致,我们也可以用前面类型检查的步骤来验证方法引用。
和静态方法引用类似,构造方法也可以通过new关键字被直接引用:
SocketImplFactory factory = MySocketImpl::new;
而对于包含有参数的函数,比如我们有一个构造函数Apple(Integer weight),我们可以采用这种方式来构造:
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(100);
数组的构造方法引用的语法则比较特殊,为了便于理解,我们可以假想存在一个接收int参数的数组构造方法。参考下面的代码:
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]
6.总结
Java8里引入了函数式编程的特性,这里每个定义的lambda表达式通过和传递的目标对象进行类型检查比较来寻找到一个匹配的结果。这样一个表达式就和一个函数式接口实现了对应的代换关系。通常我们也可以通过方法引用来重用一些已有的方法,这种方法引用的语法相当于是对lambda表达式的进一步增强,它使得我们的实现更加简练。同样,它的类型检查和lambda表达式是一样的。
三.一个挺好玩的:缓存实例的不可变类
class CacheImmutale
{
private static int MAX_SIZE = 10;
//使用数组来缓存已有的实例
private static CacheImmutale[] cache = new CacheImmutale[MAX_SIZE];
//记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;
private CacheImmutale(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public static CacheImmutale valueOf (String name)
{
//遍历已缓存的对象
for (int i = 0; i < MAX_SIZE; i++)
{
//如果已有相同实例,则直接返回该实例的缓存
if (cache[i] != null && cache[i].getName().equals(name)) {
return cache[i];
}
}
//如果缓存池已满
if(pos==MAX_SIZE)
{
//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置
cache[0] = new CacheImmutale(name);
//把pos设为1
pos = 1;
}
else
{
//把新创建的对象缓存起来,pos+1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if(obj!=null&&obj.getClass()==CacheImmutale.class)
{
CacheImmutale ci = (CacheImmutale) obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[]args)
{
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
System.out.println(c1==c2);
}
}