面向对象(下)
1. 包装类
Java 为 8 种基本数据类型分别定义了相应的引用类型,使之可以当成 Object 类型变量使用,并称之为基本数据类型的包装类。
基本数据类型和包装类的对应关系:
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
- 自动装箱:可以把一个基本类型变量直接赋值给对应的包装类变量;
- 自动拆箱:允许直接把包装类对象直接赋给一个对应的基本类型变量。
// 自动装箱
Integer inObj = 5;
// 自动拆箱
int it = inObj;
包装类可以实现基本类型变量和字符串之间的转换:
parseXxx(String s);
(除 Character 之外所有包装类都提供了该方法)valueOf(String s);
// 将字符串转换为基本类型
String intStr = "123";
int int1 = Integer.parseInt(intStr);
int int2 = Integer.valueOf(intStr);
String doubleStr = "4.56";
double double1 = Double.parseDouble(doubleStr);
double double2 = Double.valueOf(doubleStr);
String boolStr1 = "true";
String boolStr2 = "true1";
String boolStr3 = "false";
Boolean boolean1 = Boolean.parseBoolean(boolStr1);
Boolean boolean2 = Boolean.parseBoolean(boolStr2);
Boolean boolean3 = Boolean.parseBoolean(boolStr3);
System.out.println(boolean1); // true
System.out.println(boolean2); // false
System.out.println(boolean3); // false
// 将基本类型转换为字符串
String str1 = String.valueOf(6.78f);
String str2 = String.valueOf('\u9999');
System.out.println(str1); // 6.78
System.out.println(str2); // 香
// 更加简单的写法
String str3 = 5 + "";
System.out.println(str3 == "5"); // true
比较值大小:
Integer in1 = 8;
Integer in2 = 8;
Integer ina = 128;
Integer inb = 128;
Integer inc = 500;
System.out.println(in1 == in2); // true,Integer缓存了-128~127范围内的整数,可以直接判断
System.out.println(ina == inb); // false
System.out.println(Integer.compare(ina, inb)); // 0
System.out.println(Integer.compare(ina, inc)); // -1
System.out.println(Integer.compare(inc, inb)); // 1
// 比较布尔值
System.out.println(Boolean.compare(true, false)); // 1
System.out.println(Boolean.compare(true, true)); // 0
System.out.println(Boolean.compare(false, true)); // -1
2. 处理对象
2.1 toString() 方法
toString() 方法是 Object 类里的一个实例方法,所有的 Java 类都是 Object 类的子类,因此所有的 Java 对象都具有 toString() 方法。
toString() 方法是一个“自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
public class PrintObject {
public static void main(String[] args) {
Person p = new Person("孙悟空");
System.out.println(p); // Person@368239c8
System.out.println(p.toString()); // Person@368239c8
}
}
重写 toString() 方法:
class Apple {
private String color;
private double weight;
public Apple(String color, double weight) {
this.color = color;
this.weight = weight;
}
// 重写 toString() 方法
public String toString() {
return "Apple[color=" + color + ", weight=" + weight + "]";
}
}
public class PrintObject {
public static void main(String[] args) {
Apple a = new Apple("红色", 5.68);
System.out.println(a); // Apple[color=红色, weight=5.68]
}
}
2.2 == 和 equals 方法
==
:用于判断两个基本类型变量是否相等;两个字符串直接量是否相同;两个引用类型变量是否指向同一个对象;equals
:判断两个引用变量是否相等。
String str1 = "学习Java";
String str2 = "学习";
String str3 = "Java";
String str4 = "学习" + "Java";
String str5 = "学" + "习" + "Java";
String str6 = str2 + str3;
String str7 = new String("学习Java");
String str8 = new String("学习Java");
System.out.println(str1 == str4); // true
System.out.println(str1 == str5); // true
System.out.println(str1 == str6); // false
System.out.println(str1 == str7); // false
System.out.println(str7 == str8); // false
System.out.println(str1.equals(str6)); // true
System.out.println(str1.equals(str7)); // true
System.out.println(str7.equals(str8)); // true
3. 类成员
static 关键字修饰的成员就是类成员。类成员属于整个类,不属于单个实例。
单例类
大部分时候都把类的构造器定义成 public 访问权限,允许任何类自由创建该类的对象。但有时候,不需要创建这么多对象。比如:系统可能只有一个窗口管理器、一个数据库引擎访问点。如果一个类始终只能创建一个实例,则这个类被称为单例类。
class Singleton {
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 隐藏构造器
private Singleton() {}
// 提供一个静态方法,保证只产生一个 Singleton 对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true
}
}
4. final 修饰符
final 关键字可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变。
final 修饰变量时,表示该变量一旦获得了初始值就不可被改变。final 即可以修饰成员变量,也可以修饰局部变量、形参。
4.1 final 成员变量
final 修饰的成员变量必须由程序员显式地指定初始值
public class FinalVariableTest {
final int a = 6;
final String str;
final int c;
final static double d;
// 初始化块,可对没有指定默认值的实例变量指定初始值
{
str = "Hello";
}
// 静态初始化块,可对没有指定默认值的类变量指定初始值
static {
d = 5.6;
}
// 构造器
public FinalVariableTest() {
c = 5;
}
public void changeFinal() {
// 不能对 final 字段变量进行赋值
}
public static void main(String[] args) {
var ft = new FinalVariableTest();
System.out.println(ft.a); // 6
System.out.println(ft.c); // 5
System.out.println(ft.d); // 5.6
}
}
4.2 final 局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用 final 修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
public void test(final int a) {
// a = 5; // 不能对 final 修饰的形参赋值
}
public static void main(String[] args) {
final String str = "Hello";
// str = "Java"; // str 已指定默认值,无法再赋值
final int n;
n = 6;
// n = 7; // final 修饰的变量,只能赋值一次
}
4.3 final 修饰基本类型变量和引用类型变量的区别
final 修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。
final int[] arr = {5, 2, 1, 4, 3};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
arr[2] = 6;
System.out.println(Arrays.toString(arr)); // [1, 2, 6, 4, 5]
// arr = null; // 不能对 final 修饰的变量重新赋值
4.4 可执行“宏替换”的 final 变量
对一个 final 变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个 final 变量就不再是一个变量,而是相当于一个直接量。
- 使用 final 修饰符修饰
- 在定义该 final 变量时指定了初始值
- 该初始值可以在编译时就被确定下来
public static void main(String[] args) {
final int a = 5;
System.out.println(a);
}
对于这个程序来说,变量 a 其实根本不存在,当程序执行 System.out.println(a);
代码时,实际转换为执行 System.out.println(5);
。
final 修饰符的一个重要用途就是定义“宏变量”。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
如果被赋值的表达式只是基本的算术表达式或字符串连接运行,没有访问普通变量,调用方法,Java 编译器同样会将这种 final 变量当成“宏变量”处理。
4.5 final 方法
final 修饰的方法不可被重写,如果出于某些原因,不希望重写父类的某个方法,则可以使用 final 修饰该方法。
public class TestClass {
public final void test() {}
// 可以重载 final 方法
public final void test(String msg) {}
}
class Sub extends TestClass {
// public void test() {} // 不能重写 final 方法
}
4.6 final 类
final 修饰的类不可以有子类。例如:java.lang.Math 类就是一个 final 类。
public final class FinalClass {}
4.7 不可变类
不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java 提供的 8 个包装类和 java.lang.String 类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。
class Name {
private String firstName;
private String lastName;
public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName() {
return lastName;
}
}
public class Person {
private final Name name;
public Person(Name name) {
// 设置 name 实例变量为临时创建的 Name 对象
this.name = new Name(name.getFirstName(), name.getLastName());
}
public Name getName() {
// 返回一个匿名对象
return new Name(name.getFirstName(), name.getLastName());
}
public static void main(String[] args) {
Name n = new Name("悟空", "孙");
PersonTest p = new Person(n);
System.out.println(p.getName().getFirstName()); // 悟空
n.setFirstName("八戒");
System.out.println(p.getName().getFirstName()); // 悟空
System.out.println(n.getFirstName()); // 八戒
PersonTest p2 = new PersonTest(n);
System.out.println(p2.getName().getFirstName()); // 八戒
}
}
上面程序可以看出,虽然改变对象 n 的成员变量值,但是保存在对象 p 里的成员变量 name 值并未改变。因此,如果引用类型的成员变量的类是可变的,就必须采取必要措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。
5. 抽象类
有些时候,某个父类只知道其子类应该包含怎样的方法,但无法准确地知道这些子类如何实现这些方法。这时就可以使用抽象方法,抽象方法只有方法签名,没有方法实现的方法。
抽象方法和抽象类必须使用 abstract
修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
- 抽象类和抽象方法必须使用
abstract
修饰符来修饰,抽象方法不能有方法体; - 抽象类不能被实例化,无法使用
new
关键字来调用抽象类的构造器创建抽象类的实例; - 抽象类可以包含成员变量、方法、构造器、初始化块、内部类 5 种成分;
- 含有抽象方法的类,只能被定义成抽象类。
// 抽象类 - 形状
public abstract class Shape {
private String color;
// 定义一个计算周长的抽象方法
public abstract double calPerimeter();
// 定义一个返回形状的抽象方法
public abstract String getType();
// 构造器
public Shape() {}
public Shape(String color) {
this.color = color;
}
public void setColor(String color) {
this.color = color;
}
public String getColor() {
return this.color;
}
}
// 类 - 三角形
public class Triangle extends Shape {
// 定义三角形的三边
private double a;
private double b;
private double c;
public Triangle(String color, double a, double b, double c) {
super(color);
this.setSides(a, b, c);
}
public void setSides(double a, double b, double c) {
if (a >= b + c || b >= a + c || c >= a + b) {
System.out.println("三角形两边之和必须大于第三边");
return;
}
this.a = a;
this.b = b;
this.c = c;
}
// 重写Shape类的的计算周长的抽象方法
public double calPerimeter() {
return a + b + c;
}
// 重写Shape类的的返回形状的抽象方法
public String getType() {
return getColor() + "三角形";
}
}
// 类 - 圆
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
// 重写Shape类的的计算周长的抽象方法
public double calPerimeter() {
return 2 * Math.PI * radius;
}
// 重写Shape类的的返回形状的抽象方法
public String getType() {
return getColor() + "圆形";
}
public static void main(String[] args) {
Shape s1 = new Triangle("黑色", 3, 4, 5);
Shape s2 = new Circle("黄色", 3);
System.out.println(s1.getType()); // 黑色三角形
System.out.println(s1.calPerimeter()); // 12.0
System.out.println(s2.getType()); // 黄色圆形
System.out.println(s2.calPerimeter()); // 18.84955592153876
}
}
6. 接口
6.1 接口的概念和定义
接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计哲学。
[修饰符] interface 接口名 {
常量定义
抽象方法定义
内部类、接口、枚举定义
私有方法、默认方法、类方法定义
}
- 修饰符可以是 public 或者省略,如果省略了 public 修饰符,则默认采用包权限访问控制符;
- 接口名应与类名采用相同的命名规范;
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类;
- 接口中定义成员变量时,不管是否使用修饰符,成员变量总是总是使用 public static final 修饰符修饰;
- 系统将自动为接口内普通方法增加 public abstract 修饰符,且不能有方法体。
public interface Output {
// 接口里定义的成员变量只能是常量,系统会默认加上 public static final 修饰符
int MAX_CACHE_LINE = 50;
// 普通方法
void out();
void getData(String msg);
// 默认方法
default void print(String... msgs) {
for (String msg : msgs) {
System.out.println(msg);
}
}
default void test() {
System.out.println("默认的 test() 方法");
}
// 类方法
static void staticTest() {
System.out.println("类方法");
}
// 私有方法
private void foo() {
System.out.println("私有方法");
}
// 私有类方法
private static void bar() {
System.out.println("私有静态方法");
}
}
6.2 接口的继承
[修饰符] interface 接口名 extends 父接口1, 父接口2... {
...
}
interface InterfaceA {
int PROP_A = 5;
void testA();
}
interface InterfaceB {
int PROP_B = 6;
void testB();
}
interface InterfaceC extends InterfaceA, InterfaceB {
int PROP_C = 7;
void testC();
}
public class InterfaceTest {
public static void main(String[] args) {
System.out.println(InterfaceC.PROP_A); // 5
System.out.println(InterfaceC.PROP_B); // 6
System.out.println(InterfaceC.PROP_C); // 7
}
}
6.3 使用接口
[修饰符] class 类名 implements 接口1, 接口2... {
...
}
// 定义一个 Product 接口
interface Product {
int getProduceTime();
}
// 让 Printer 类实现 Output 和 Product 接口
public class Printer implements Output, Product {
private String[] printData = new String[MAX_CACHE_LINE];
private int dataNum = 0; // 记录当前打印数
public void out() {
// 只要还有作业,就继续打印
while (dataNum > 0) {
System.out.println("打印机打印:" + printData[0]);
// 把作业队列整体前移一位,并将剩下的作业数减 1
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE) {
System.out.println("输出队列已满,添加失败");
} else {
// 把打印数据添加到队列里,已保存数据的数理加 1
printData[dataNum++] = msg;
}
}
public int getProduceTime () {
return 45;
}
public static void main(String[] args) {
Output o = new Printer();
o.getData("道可道,非常道;,名可名,非常名。");
o.getData("无,名天地之始;有,名万物之母。");
o.out();
// --> 打印机打印:道可道,非常道;,名可名,非常名。
// --> 打印机打印:无,名天地之始;有,名万物之母。
o.getData("故常无,欲以观其妙;常有,欲以观其徼。");
o.getData("此两者同出而异名,同谓之玄,玄之又玄,众妙之门。");
o.out();
// --> 打印机打印:故常无,欲以观其妙;常有,欲以观其徼。
// --> 打印机打印:此两者同出而异名,同谓之玄,玄之又玄,众妙之门。
o.print("孙悟空", "猪八戒", "白骨精");
// --> 孙悟空
// --> 猪八戒
// --> 白骨精
Product p = new Printer();
System.out.println(p.getProduceTime()); // 45
}
}
6.4 接口和抽象类
接口和抽象类都剧透如下特征:
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承;
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
6.5 面向接口编程
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
1. 简单工厂模式
// 定义 Computer 类
public class Computer {
private Output out;
public Computer (Output out) {
this.out = out;
}
public void keyIn (String msg) {
out.getData(msg);
}
public void print() {
out.out();
}
}
public class OutputFactory {
// Output 工厂,负责生成 Output 对象
public Output getOutput() {
return new Printer();
}
public static void main(String[] args) {
OutputFactory of = new OutputFactory();
Computer c = new Computer(of.getOutput());
c.keyIn("天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。");
c.keyIn("故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。");
c.print();
// --> 打印机打印:天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。
// --> 打印机打印:故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。
}
}
如果我们有了更好的 BetterPrinter 需要替换旧的 Printer:
// 定义一个新的更好的 BetterPrinter 类
public class BetterPrinter implements Output {
private String[] printData = new String[MAX_CACHE_LINE * 2];
private int dataNum = 0;
public void out() {
while (dataNum > 0) {
System.out.println("高速打印机正在打印:" + printData[0]);
System.arraycopy(printData, 1, printData, 0, --dataNum);
}
}
public void getData(String msg) {
if (dataNum >= MAX_CACHE_LINE * 2) {
System.out.println("输出队列已满,添加失败");
} else {
printData[dataNum++] = msg;
}
}
}
只需更改一步就可以了:
public class OutputFactory {
// Output 工厂,负责生成 Output 对象
public Output getOutput() {
// return new Printer(); // 只要改变这里就可以了
return new BetterPrinter();
}
public static void main(String[] args) {
OutputFactory of = new OutputFactory();
Computer c = new Computer(of.getOutput());
c.keyIn("天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。");
c.keyIn("故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。");
c.print();
// --> 高速打印机正在打印:天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。
// --> 高速打印机正在打印:故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。
}
}
2. 命令模式
先使用一个 Command 接口来定义一个方法,用这个方法来封装“处理行为”:
public interface Command {
void process(int element);
}
分别定义不同的方法:
// 直接打印元素
public class PrintCommand implements Command {
public void process(int element) {
System.out.println("迭代输出目标数组的元素:" + element);
}
}
// 打印元素的平方
public class SquareCommand implements Command {
public void process(int element) {
System.out.println("数组元素的平方是:" + (element * element));
}
}
命令模式的使用:
public class ProcessArray {
// 只有当调用此方法时,才真正传入一个 Command 对象,才确定对数组的处理行为
public void process(int[] target, Command cmd) {
for (int t : target) {
cmd.process(t);
}
}
}
public class CommandTest {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
pa.process(target, new PrintCommand());
System.out.println("=============");
pa.process(target, new SquareCommand());
}
}
结果为:
迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
=============
数组元素的平方是:9
数组元素的平方是:16
数组元素的平方是:36
数组元素的平方是:16
7. 内部类
把一个类放在另一个类的内部定义,这个类被称为内部类,包含内部类的类也被称为外部类。
- 内部类比外部类多使用三个修饰符:private、protected、static;
- 非静态内部类不能拥有静态成员。
7.1 非静态内部类
public class Outer {
private int prop = 9;
private String outerStr = "外部 private 变量";
class Inner {
private int prop = 8;
public void info() {
System.out.println(prop); // 8
System.out.println(this.prop); // 8
System.out.println(Outer.this.prop); // 9
System.out.println(outerStr); // 外部 private 变量
}
}
static class StaticInner {
private static int prop = 6;
public void info() {
System.out.println("static 内部类");
}
}
public void test() {
// 内部调用内部类
System.out.println(new Inner().prop); // 8
System.out.println(StaticInner.prop); // 6
}
public static void main(String[] args) {
new Outer().test();
// 在外部使用内部类
Outer.Inner in1 = new Outer().new Inner();
in1.info();
// new Inner(); // 不能访问非静态内部类
new StaticInner().info(); // static 内部类
System.out.println(StaticInner.prop); // 6
// 在外部类以外使用静态内部类
Outer.StaticInner in = new Outer.StaticInner();
in.info(); // static 内部类
}
}
非静态内部类里不能有静态方法、静态成员变量、静态初始化块。
7.2 静态内部类
静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。
7.3 使用内部类
- 在外部类内部使用内部类:
Inner in = new Inner();
- 在外部类以外使用非静态内部类:
Outer.Inner in = new Outer().new Inner();
- 在外部类以外使用静态内部类:
Outer.Inner in = new Outer.Inner();
7.4 局部内部类
如果把一个类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能再外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和 static 修饰符修饰。
7.5 匿名内部类
匿名内部类适合创建那些只需一次使用的类。创建匿名内部类时会创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
- 匿名内部类不能是抽象类
- 匿名内部类不能定义构造器
interface Product1 {
double getPrice();
String getName();
}
public class AnonymousTest {
public void test(Product1 p) {
System.out.println("购买了一个" + p.getName() + "花掉了" + p.getPrice());
}
public static void main(String[] args) {
// 匿名内部类访问的局部变量,必须使用 final 修饰。
// Java 8 以后的版本可以省略 final 不写,但必须按照有 final 修饰的方式来用 —— 即一次赋值后,不能再重新赋值。
String name = "老王";
AnonymousTest ta = new AnonymousTest();
// 使用匿名内部类实例
ta.test(new Product1() {
{
System.out.println("土豪" + name);
}
public double getPrice() {
return 12999.0;
}
public String getName() {
return "华硕 RTX-2080Ti ";
}
});
}
}
// 输出结果:
// --> 土豪老王
// --> 购买了一个华硕 RTX-2080Ti 花掉了12999.0
8. Lambda 表达式
Lambda 表示式是 Java 8 的重要更新。Lambda 表示式支持将代码块作为方法参数,允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
// 方式一:
(形参列表) -> {
// 代码块;
}
// 只有一个形参,简写方式:
形参 -> {
// 代码块;
}
// 没有形参,简写方式:
() -> {
// 代码块;
}
// 只有一条语句,简写方式:
(a, b) -> a = b
class ProcessArray {
public void process(int[] target, Command cmd) {
for (int t : target) {
cmd.process(t);
}
}
}
public class LambdaTest {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 使用匿名内部类
pa.process(array, new Command() {
@Override
public void process(int e) {
System.out.println("数组元素 " + e + " 的平方是:" + e * e);
}
});
// 使用 Lambda 表达式,更加简洁明了
System.out.println("====================");
pa.process(array, (int e) -> {
System.out.println("数组元素 " + e + " 的平方是:" + e * e);
});
}
}
Lambda 表达式的两个限制:
- Lambda 表达式的目标类型必须是明确的函数式接口;
- Lambda 表达式只能为函数式接口创建对象。Lambda 表达式只能实现一个方法,因此只能为只有一个抽象方法的接口(函数式接口)创建对象。
Lambda 表达式对方法和构造器的引用:
import java.util.Arrays;
@FunctionalInterface
interface Command {
void process(int element);
}
@FunctionalInterface
interface Converter {
Integer convert(String from);
}
@FunctionalInterface
interface MyTest {
String test(String str, int a, int b);
}
@FunctionalInterface
interface MyTest2 {
PersonTest setName(String name);
}
class PersonTest {
private String name;
public PersonTest(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class LambdaTest {
public static void main(String[] args) {
// 1.引用类方法,函数式接口中被实现方法的全部参数传递给该类方法作为参数
Converter c1 = Integer::valueOf;
System.out.println(c1.convert("129")); // 129
// 2.引用特定对象的实例方法,函数式接口中被实现方法的全部参数传递给该方法作为参数
Converter c2 = "君不见黄河之水天上来"::indexOf;
System.out.println(c2.convert("黄")); // 3
// 3.引用某类对象的实例方法,第一个参数作为调用者,后面的参数全部传递给该方法作为参数
MyTest mt = String::substring;
System.out.println(mt.test("君不见黄河之水天上来", 3, 7)); // 黄河之水
// 4.引用构造器,函数式接口中被实现方法的全部参数传递给该构造器作为参数
MyTest2 mt2 = PersonTest::new;
mt2.setName("老王");
// 使用 Lambda 表达式调用 Arrays 的类方法
String[] arr1 = {"埃斯库罗斯", "白居易", "仓央嘉措", "李白"};
Arrays.parallelSort(arr1, (a, b) -> a.length() - b.length());
System.out.println(Arrays.toString(arr1)); // [李白, 白居易, 仓央嘉措, 埃斯库罗斯]
int[] arr2 = {3, -4, 25, 16, 30, 18};
Arrays.parallelPrefix(arr2, (l, r) -> l + r);
System.out.println(Arrays.toString(arr2)); // [3, -1, 24, 40, 70, 88]
int[] arr3 = new int[5];
Arrays.parallelSetAll(arr3, index -> index * 5);
System.out.println(Arrays.toString(arr3)); // [0, 5, 10, 15, 20]
}
}
9. 枚举类
在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有4个对象。这种实例有限且固定的类,在 Java 里被称为枚举类。
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
枚举类与普通类的区别:
- 枚举类可以实现一个或多个接口,使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,而不是默认继承 Object 类,因此枚举类不能显式继承其他父类;
- 使用 enum 定义、非抽象的枚举类默认会使用 final 修饰;
- 枚举类的构造器只能使用 private 访问控制符,如果省略了构造器的访问控制符,则默认使用 private 修饰;如果强制指定访问控制符,则只能指定 private 修饰符。由于枚举类的所有构造器都是 private 的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类;
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加 public static final 修饰,无须程序员显式添加。
public class EnumTest {
private void judge(Season s) {
switch (s) {
case SPRING:
System.out.println("春天到了,播种的季节");
break;
case SUMMER:
System.out.println("夏天到了,游泳的季节");
break;
case AUTUMN:
System.out.println("秋天到了,收获的季节");
break;
case WINTER:
System.out.println("冬天到了,下雪的季节");
break;
default:
System.out.println("你已跳出四季轮回,祝你渡劫成功!");
}
}
public static void main(String[] args) {
// 使用枚举类默认的 values() 方法,返回该枚举类的所有实例
for (Season s : Season.values()) {
System.out.println(s);
}
// 使用枚举类实例
new EnumTest().judge(Season.AUTUMN); // 秋天到了,收获的季节
}
}
10. 修饰符的适用范围
外部类/接口 | 成员属性 | 方法 | 构造器 | 初始化块 | 成员内部类 | 局部成员 | |
---|---|---|---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ | ✔ | ||
protected | ✔ | ✔ | ✔ | ✔ | |||
包访问控制符 | ✔ | ✔ | ✔ | ✔ | ✔ | ||
private | ✔ | ✔ | ✔ | ✔ | |||
abstract | ✔ | ✔ | ✔ | ||||
final | ✔ | ✔ | ✔ | ✔ | ✔ | ||
static | ✔ | ✔ | ✔ | ✔ | |||
strictfp | ✔ | ✔ | |||||
synchronized | ✔ | ||||||
native | ✔ | ||||||
transient | ✔ | ||||||
volatile | ✔ | ||||||
default | ✔ |
- strictfp 关键字含义是 FP-strict,也就是精确浮点的意思。使用了此修饰符的类、接口、方法,可以在进行浮点运算时更加精确。
- native 关键字修饰的方法类似于一个抽象方法,通常采用 C 语言来实现。如果某个方法需要利用平台相关特性,或者访问系统硬件等,则可以使用 native 修饰该方法,再把该方法交给 C 去实现。一旦 Java 程序中包含了 native 方法,这个程序将失去跨平台的功能。
- 4 个访问控制符是互斥的,最多只能出现其中之一。
- abstract 和 final 永远不能同时使用。
- abstract 和 static 不能同时修饰方法,可以同时修饰内部类。
- abstract 和 private 不能同时修饰方法,可以同时修饰内部类。
- private 和 final 同时修饰方法虽然语法是合法的,但没有太大意义(private 修饰的方法不可能被子类重写,因此使用 final 修饰没有意义)
11. 练习
- 通过抽象类定义车类的模板,然后通过抽象的车类来派生拖拉机、卡车、小轿车。
- 定义一个接口,并使用匿名内部类方式创建接口的实例。
- 定义一个函数式接口,并使用 Lambda 表达式创建函数式接口的实例。
- 定义一个类,该类用于封装一桌梭哈游戏,这个类应该包含桌上剩下的牌的信息,并包含 5 个玩家的状态信息:他们各自的位置、游戏状态(正在游戏或已放弃)、手上已有的牌等信息。如果有可能,这个类还应该实现发牌方法,这个方法需要控制从谁开始发牌,不要发牌给放弃的人,并修改桌上剩下的牌。
- 将学到的知识和遇到的问题,整理成笔记,记录下来