2020重新出发,JAVA基础,内部类
@
内部类
在类内部可定义成员变量和方法,且在类内部也可以定义另一个类。如果在类 Outer 的内部再定义一个类 Inner,此时类 Inner 就称为内部类(或称为嵌套类),而类 Outer 则称为外部类(或称为宿主类)。
内部类可以很好地实现隐藏,一般的非内部类是不允许有 private 与 protected 权限的,但内部类可以。内部类拥有外部类的所有元素的访问权限。
内部类可以分为:实例内部类、静态内部类和成员内部类,每种内部类都有它特定的一些特点
内部类也可以分为多种形式,与变量非常类似,如图所示。
内部类的特点如下:
- 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的
.class
文件,但是前面冠以外部类的类名和$
符号。 - 内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否为 private。
- 内部类声明成静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。
内部类的使用方法非常简单,例如下面的代码演示了内部类最简单的应用。
public class Test {
public class InnerClass {
public int getSum(int x,int y) {
return x + y;
}
}
public static void main(String[] args) {
Test.InnerClass ti = new Test().new InnerClass();
int i = ti.getSum(2,3);
System.out.println(i); // 输出5
}
}
有关内部类的说明有如下几点。
- 外部类只有两种访问级别:public 和默认;内部类则有 4 种访问级别:public、protected、 private 和默认。
- 在外部类中可以直接通过内部类的类名访问内部类。
InnerClass ic = new InnerClass(); // InnerClass为内部类的类名
- 在外部类以外的其他类中则需要通过内部类的完整类名访问内部类。
Test.InnerClass ti = newTest().new InnerClass(); // Test.innerClass是内部类的完整类名
- 内部类与外部类不能重名。
提示:内部类的很多访问规则可以参考变量和方法。另外使用内部类可以使程序结构变得紧凑,但是却在一定程度上破坏了 Java 面向对象的思想。
实例内部类
实例内部类是指没有用 static 修饰的内部类,有的地方也称为非静态内部类。
public class Outer {
class Inner {
// 实例内部类
}
}
上述示例中的 Inner 类就是实例内部类。
实例内部类有如下特点。
1)在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例。
2)在实例内部类中,可以访问外部类的所有成员。
提示:如果有多层嵌套,则内部类可以访问所有外部类的成员。
3)在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A 包含内部类 B,类 B 中包含内部类 C,则在类 A 中不能直接访问类 C,而应该通过类 B 的实例去访问类 C。
4)外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。
如果实例内部类 B 与外部类 A 包含有同名的成员 t,则在类 B 中 t 和 this.t 都表示 B 中的成员 t,而 A.this.t 表示 A 中的成员 t。
5)在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。
静态内部类
静态内部类是指使用 static 修饰的内部类。示例代码如下:
public class Outer {
static class Inner {
// 静态内部类
}
}
上述示例中的 Inner 类就是静态内部类。静态内部类有如下特点。
1)在创建静态内部类的实例时,不需要创建外部类的实例。
2)静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
3)静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
局部内部类
局部内部类是指在一个方法中定义的内部类。示例代码如下:
public class Test {
public void method() {
class Inner {
// 局部内部类
}
}
}
局部内部类有如下特点:
1)局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰。
2)局部内部类只在当前方法中有效。
3)局部内部类中不能定义 static 成员。
4)局部内部类中还可以包含内部类,但是这些内部类也不能使用访问控制修饰符(public、private 和 protected)static 修饰符修饰。
5)在局部内部类中可以访问外部类的所有成员。
6)在局部内部类中只可以访问当前方法中 final 类型的参数与变量。如果方法中的成员与外部类中的成员同名,则可以使用 <OuterClassName>.this.<MemberName>
的形式访问外部类中的成员。
匿名内部类
匿名类是指没有类名的内部类,必须在创建时使用 new 语句来声明类。其语法形式如下:
new <类或接口>() {
// 类的主体
};
这种形式的 new 语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口。
使用匿名类可使代码更加简洁、紧凑,模块化程度更高。
匿名类有两种实现方式:
- 继承一个类,重写其方法。
- 实现一个接口(可以是多个),实现其方法。
下面通过代码来说明。
public class Out {
void show() {
System.out.println("调用 Out 类的 show() 方法");
}
}
public class TestAnonymousInterClass {
// 在这个方法中构造一个匿名内部类
private void show() {
Out anonyInter = new Out() {
// 获取匿名内部类的实例
void show() {
System.out.println("调用匿名类中的 show() 方法");
}
};
anonyInter.show();
}
public static void main(String[] args) {
TestAnonymousInterClass test = new TestAnonymousInterClass();
test.show();
}
}
程序的输出结果如下:
调用匿名类中的 show() 方法
从输出结果可以看出,匿名内部类有自己的实现。
提示:匿名内部类实现一个接口的方式与实现一个类的方式相同。
匿名类有如下特点:
1)匿名类和局部内部类一样,可以访问外部类的所有成员。如果匿名类位于一个方法中,则匿名类只能访问方法中 final 类型的局部变量和参数。
2)匿名类中允许使用非静态代码块进行成员初始化操作。
3)匿名类的非静态代码块会在父类的构造方法之后被执行。
Java8新特性:Effectively final
Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。
因为系统会默认添加 final 修饰符,所以在匿名内部类中直接使用非 final 变量,而 final 修饰的局部变量不能在被重新赋值。也就是说从 Java 8 开始,它不要求程序员必须将访问的局部变量显式的声明为 final 的。只要该变量不被重新赋值就可以。
一个非 final 的局部变量或方法参数,其值在初始化后就从未更改,那么该变量就是 effectively final。在 Lambda 表达式中,使用局部变量的时候,也要求该变量必须是 final 的,所以 effectively final 在 Lambda 表达式上下文中非常有用。
Lambda 表达式在编程中是经常使用的,而匿名内部类是很少使用的。那么,我们在 Lambda 编程中每一个被使用到的局部变量都去显示定义成 final 吗?显然这不是一个好方法。所以,Java 8 引入了 effectively final 新概念。
总结一下,规则没有改变,Lambda 表达式和匿名内部类访问的局部变量必须是 final 的,只是不需要程序员显式的声明变量为 final 的,从而节省时间。
Lambda表达式
Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)。
Lambda 表达式是推动 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数(函数作为参数传递进方法中),
例 先定义一个计算数值的接口,代码如下。
// 可计算接口
public interface Calculable {
// 计算两个int数值
int calculateInt(int a, int b);
}
Calculable 接口只有一个方法 calculateInt,参数是两个 int 类型,返回值也是 int 类型。实现方法代码如下:
public class Test{
/**
* 通过操作符,进行计算
*
* @param opr 操作符
* @return 实现Calculable接口对象
*/
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// 匿名内部类实现Calculable接口
result = new Calculable() {
// 实现加法运算
@Override
public int calculateInt(int a, int b) {
return a + b;
}
};
} else {
// 匿名内部类实现Calculable接口
result = new Calculable() {
// 实现减法运算
@Override
public int calculateInt(int a, int b) {
return a - b;
}
};
}
return result;
}
}
方法 calculate 中 opr 参数是运算符,返回值是实现 Calculable 接口对象。代码第 13 行和第 23 行都采用匿名内部类实现 Calculable 接口。代码第 16 行实现加法运算。代码第 26 行实现减法运算。
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 实现加法计算Calculable对象
Calculable f1 = calculate('+');
// 实现减法计算Calculable对象
Calculable f2 = calculate('-');
// 调用calculateInt方法进行加法计算
System.out.println(n1 + "+" + n2 + "=" + f1.calculateInt(n1, n2));
// System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2));
// 调用calculateInt方法进行减法计算
System.out.println(n1 + "-" + n2 + "=" + f1.calculateInt(n1, n2));
// System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2));
}
代码第 5 行中 f1 是实现加法计算 Calculable 对象,代码第 7 行中 f2 是实现减法计算 Calculable 对象。代码第 9 行和第 12 行才进行方法调用。
Java 中常见的输出函数:
- printf 主要继承了C语言中 printf 的一些特性,可以进行格式化输出。
- print 就是一般的标准输出,但是不换行。
- println 和 print 基本没什么差别,就是最后会换行。
输出结果如下:
10+5=15
10-5=15
例 1 使用匿名内部类的方法 calculate 代码很臃肿,Java 8 采用 Lambda 表达式可以替代匿名内部类。修改之后的通用方法 calculate 代码如下:
/**
* 通过操作符,进行计算
* @param opr 操作符
* @return 实现Calculable接口对象
*/
public static Calculable calculate(char opr) {
Calculable result;
if (opr == '+') {
// Lambda表达式实现Calculable接口
result = (int a, int b) -> {
return a + b;
};
} else {
// Lambda表达式实现Calculable接口
result = (int a, int b) -> {
return a - b;
};
}
return result;
}
代码第 10 行和第 15 行用 Lambda 表达式替代匿名内部类,可见代码变得简洁。通过以上示例我们发现,Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。
Lambda 表达式标准语法形式如下:
(参数列表) -> {
// Lambda表达式体
}
->
被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:
- 左侧:Lambda 表达式的参数列表。
- 右侧:Lambda 表达式中所需执行的功能,用
{ }
包起来,即 Lambda 体。
Java Lambda 表达式的优缺点
优点:
- 代码简洁,开发迅速
- 方便函数式编程
- 非常容易进行并行计算
- Java 引入 Lambda,改善了集合操作(引入 Stream API)
缺点:
- 代码可读性变差
- 在非并行计算中,很多计算未必有传统的 for 性能要高
- 不容易进行调试
函数式接口
Lambda 表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误:
The target type of this expression must be a functional interface
这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface,示例代码如下。
// 可计算接口@FunctionalInterfacepublic interface Calculable { // 计算两个int数值 int calculateInt(int a, int b);}
在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。
@FunctionalInterface 注解与 @Override 注解的作用类似。Java 8 中专门为函数式接口引入了一个新的注解 @FunctionalInterface。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
提示:Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。
Lambda表达式的使用
访问变量
Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量。
访问成员变量
成员变量包括实例成员变量和静态成员变量。
在 Lambda 表达式中可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。
public class LambdaDemo {
// 实例成员变量
private int value = 10;
// 静态成员变量
private static int staticValue = 5;
// 静态方法,进行加法运算
public static Calculable add() {
Calculable result = (int a, int b) -> {
// 访问静态成员变量,不能访问实例成员变量
staticValue++;
int c = a + b + staticValue;
// this.value;
return c;
};
return result;
}
// 实例方法,进行减法运算
public Calculable sub() {
Calculable result = (int a, int b) -> {
// 访问静态成员变量和实例成员变量
staticValue++;
this.value++;
int c = a - b - staticValue - this.value;
return c;
};
return result;
}
}
LambdaDemo 类中声明一个实例成员变量 value 和一个静态成员变量 staticValue。此外,还声明了静态方法 add(见代码第 8 行)和实例方法 sub(见代码第 20 行)。add 方法是静态方法,静态方法中不能访问实例成员变量,所以代码第 13 行的 Lambda 表达式中也不能访问实例成员变量,也不能访问实例成员方法。
sub 方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以代码第 23 行的 Lambda 表达式中可以访问这些变量,当然实例方法和静态方法也可以访问。当访问实例成员变量或实例方法时可以使用 this,如果不与局部变量发生冲突情况下可以省略 this。
访问局部变量
对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)。
public class LambdaDemo {
// 实例成员变量
private int value = 10;
// 静态成员变量
private static int staticValue = 5;
// 静态方法,进行加法运算
public static Calculable add() {
// 局部变量
int localValue = 20;
Calculable result = (int a, int b) -> {
// localValue++;
// 编译错误
int c = a + b + localValue;
return c;
};
return result;
}
// 实例方法,进行减法运算
public Calculable sub() {
// final局部变量
final int localValue = 20;
Calculable result = (int a, int b) -> {
int c = a - b - staticValue - this.value;
// localValue = c;
// 编译错误
return c;
};
return result;
}
}
上述代码第 10 行和第 23 行都声明一个局部变量 localValue,Lambda 表达式中访问这个变量,如代码第 14 行和第 25 行。不管这个变量是否显式地使用 final 修饰,它都不能在 Lambda 表达式中修改变量,所以代码第 12 行和第 26 行如果去掉注释会发生编译错误。
注意:Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。
方法引用
方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。
Java 8 之后增加了双冒号::
运算符,该运算符用于“方法引用”,注意不是调用方法。“方法引用”虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关。 方法引用的语法格式如下:
ObjectRef::methodName
其中,ObjectRef 是类名或者实例名,methodName 是相应的方法名。
注意:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致,示例代码如下。
public class LambdaDemo {
// 静态方法,进行加法运算
// 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
public static int add(int a, int b) {
return a + b;
}
// 实例方法,进行减法运算
// 参数列表要与函数式接口方法calculateInt(int a, int b)兼容
public int sub(int a, int b) {
return a - b;
}
}
LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。
public class HelloWorld {
public static void main(String[] args) {
int n1 = 10;
int n2 = 5;
// 打印加法计算结果
display(LambdaDemo::add, n1, n2);
LambdaDemo d = new LambdaDemo();
// 打印减法计算结果
display(d::sub, n1, n2);
}
/**
* 打印计算结果
*
* @param calc Lambda表达式
* @param n1 操作数1
* @param n2 操作数2
*/
public static void display(Calculable calc, int n1, int n2) {
System.out.println(calc.calculateInt(n1, n2));
}
}
代码第 18 行声明 display 方法,第一个参数 calc 是 Calculable 类型,它可以接受三种对象:Calculable 实现对象、Lambda 表达式和方法引用。代码第 6 行中第一个实际参数LambdaDemo::add
是静态方法的方法引用。代码第 9 行中第一个实际参数d::sub
,是实例方法的方法引用,d 是 LambdaDemo 实例。
提示:代码第 6 行的LambdaDemo::add
和第 9 行的d::sub
是方法引用,此时并没有调用方法,只是将引用传递给 display 方法,在 display 方法中才真正调用方法。