内部类、枚举与泛型:把对象和类型边界表达清楚
掌握匿名内部类、付款枚举、泛型约束与对象相等规则。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day03|面向对象进阶(三)
本日把”对象写在哪儿”和”类型怎么约束”两件事讲清楚:先看内部类的四种形式(成员、静态、局部、匿名),再用枚举把一组固定取值变成有类型的常量,最后用泛型把容器的元素类型交给编译期检查。课程后半段顺带进入 java.lang 包常用 API 的第一站:Object、Objects、包装类、StringBuilder、StringJoiner。
学完应能:写出四种内部类并说明访问规则;用枚举替代”魔法常量”作为参数;定义泛型类/接口/方法并正确使用通配符上下限;知道泛型擦除与基本类型限制;正确重写 toString()/equals(),并优先用 Objects.equals() 做比较。
1. 内部类概述
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块)。如果一个类定义在另一个类的内部,这个类就是内部类。
使用场景:当一个类的内部,包含了一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
public class Car {
// 内部类
public class Engine {
}
}
课件把内部类分为四种形式,并标注了重要程度:
| 序号 | 形式 | 重要程度 |
|---|---|---|
| 01 | 成员内部类 | 了解 |
| 02 | 静态内部类 | 了解 |
| 03 | 局部内部类 | 了解 |
| 04 | 匿名内部类 | 重点 |
课件对局部内部类的评价是”鸡肋语法,看看就好”——知道语法、能读懂代码即可,实际开发中很少用。
2. 成员内部类[了解]
2.1 定义与创建对象
成员内部类就是类中的一个普通成员,类似前面学过的普通成员变量、成员方法;它没有 static 修饰,属于外部类的对象持有,必须先有外部类对象,才能有成员内部类对象。
创建对象的格式:
外部类名.内部类名 对象名 = new 外部类(...).new 内部类(...);
Outer.Inner in = new Outer().new Inner();
public class Outer {
// 成员内部类
public class Inner {
}
}
注意:JDK 16 之前,成员内部类中不能定义静态成员;JDK 16 开始也可以定义静态成员了。
2.2 成员内部类中访问其他成员的特点
- 和前面学过的实例方法一样,成员内部类的实例方法中,同样可以直接访问外部类的实例成员、静态成员。
- 可以在成员内部类的实例方法中拿到当前外部类对象,格式是
外部类名.this。
public class People {
private int heartBeat = 110;
// 成员内部类
public class Heart {
private int heartBeat = 95;
public void show() {
int heartBeat = 80;
System.out.println(heartBeat); // 80,就近取局部变量
System.out.println(this.heartBeat); // 95,内部类自己的成员
System.out.println(People.this.heartBeat); // 110,外部类对象.this
}
}
}
public class Outer {
public static String schoolName = "黑马";
public static void inAddr() {
System.out.println("我们在深圳黑马!");
}
private String hobby;
private double height;
public void run() {
System.out.println("run");
}
// 成员内部类:无 static 修饰,属于外部类的对象持有
public class Inner {
public void show() {
// 1、可以直接访问外部类的静态成员
System.out.println(schoolName);
inAddr();
// 2、可以直接访问外部类的实例成员
System.out.println(hobby);
System.out.println(height);
run();
}
}
}
class Test {
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
Outer.Inner in = new Outer().new Inner();
in.show();
}
}
小结(课件自测页的口径):
- 成员内部类是什么?如何创建其对象?—— 就是类中的一个普通成员,类似普通成员变量、成员方法;
外部类名.内部类名 对象名 = new 外部类(...).new 内部类(...); - 成员内部类的实例方法中访问其他成员有啥特点?—— 可以直接访问外部类的实例成员、静态成员;可以拿到当前外部类对象,格式是
外部类名.this。
3. 静态内部类[了解]
什么是静态内部类? 有 static 修饰的内部类,属于外部类自己持有(不依赖外部类对象)。
public class Outer {
// 静态内部类
public static class Inner {
}
}
创建对象的格式:
外部类名.内部类名 对象名 = new 外部类.内部类(...);
Outer.Inner in = new Outer.Inner();
静态内部类中访问外部类成员的特点:可以直接访问外部类的静态成员,不可以直接访问外部类的实例成员。
public class Outer {
public static String schoolName = "黑马";
private double height;
public static class Inner {
public void show() {
System.out.println(schoolName); // 可以:访问外部类静态成员
// System.out.println(height); // 不可以:不能直接访问外部类实例成员
}
}
}
class Test {
public static void main(String[] args) {
Outer.Inner in = new Outer.Inner();
in.show();
}
}
对比记忆:成员内部类靠外部对象活,静态内部类靠外部类活。
4. 局部内部类[了解]
局部内部类定义在方法中、代码块中、构造器等执行体中。
public class Test {
public static void main(String[] args) {
}
public static void go() {
class A { // 局部类
}
abstract class B { // 局部抽象类
}
interface C { // 局部接口
}
}
}
局部内部类只能在该方法/代码块的局部作用域内使用。课件提示这是”鸡肋语法”,重点在于能读懂,以及它是理解匿名内部类的前置知识(匿名内部类就是一种特殊的局部内部类)。
5. 匿名内部类[重点]
5.1 认识匿名内部类
匿名内部类就是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
格式:
new 类或接口(参数值...) {
类体(一般是方法重写);
};
new Animal() {
@Override
public void cry() {
}
};
- 特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
- 作用:用于更方便地创建一个子类对象。
public class Test {
public static void main(String[] args) {
// 匿名内部类本质是一个子类,同时会立即创建一个子类对象
Animal a = new Animal() {
@Override
public void cry() {
System.out.println("🐕是汪汪汪的叫~~~");
}
};
a.cry();
}
}
abstract class Animal {
public abstract void cry();
}
上面这段代码等价于”先写一个 Dog extends Animal 的子类,再 new Dog()”,但省掉了一个只用一次的子类文件。
5.2 匿名内部类的常见使用场景
通常作为一个参数传输给方法。
需求:猫、狗参加游泳比赛。
public interface Swimming {
void swim();
}
public class Test {
public static void main(String[] args) {
// 目标:认识匿名内部类,并掌握其作用。
Swimming s1 = new Swimming() {
@Override
public void swim() {
System.out.println("狗🏊飞快~~~");
}
};
go(s1);
go(new Swimming() {
@Override
public void swim() {
System.out.println("猫🏊也还行~~~");
}
});
}
public static void go(Swimming s) {
System.out.println("开始============");
s.swim();
}
}
小结(课件自测页的口径):
| 问题 | 回答 |
|---|---|
| 书写格式是什么? | new 类或接口(参数值...) { 类体(一般是方法重写); }; |
| 有啥特点? | 本质就是一个子类,并会立即创建出一个子类对象 |
| 有啥作用、应用场景? | 可以更方便地创建出一个子类对象;通常作为一个参数传输给方法 |
6. 枚举
6.1 认识枚举
枚举是一种特殊类。格式:
修饰符 enum 枚举类名 {
名称1, 名称2, ...;
其他成员...
}
public enum A {
X, Y, Z;
}
注意:枚举类中的第一行,只能写一些合法的标识符(名称),多个名称用逗号隔开。这些名称本质是常量,每个常量都会记住枚举类的一个对象。
6.2 枚举类的特点
- 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
- 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
- 枚举都是最终类,不可以被继承。
- 枚举类中,从第二行开始,可以定义类的其他各种成员。
- 编译器为枚举类新增了几个方法,并且枚举类都是继承
java.lang.Enum类的,从Enum类也会继承到一些方法。
从编译器视角看,一个枚举类编译后大致长这样(课件给出的反编译结果):
// Compiled from "A.java"
public final class A extends java.lang.Enum<A> {
public static final A X = new A();
public static final A Y = new A();
public static final A Z = new A();
public static A[] values();
public static A valueOf(java.lang.String);
}
这解释了为什么”枚举不能被继承”(final class)、“枚举不能对外创建对象”(构造器私有)以及 values()、valueOf() 从哪来。
public enum A {
// 枚举类的第一行必须罗列的是枚举对象的名称
X, Y, Z;
// 从第二行开始可以定义类的其他各种成员
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Test {
public static void main(String[] args) {
// A a = new A(); // 报错:枚举类对外不能创建对象
A a1 = A.X;
A a2 = A.Y;
// values():拿到枚举类的全部对象,放到一个数组中返回
A[] as = A.values();
for (int i = 0; i < as.length; i++) {
System.out.println(as[i]);
}
// valueOf():按名称拿到对应的枚举对象
A y = A.valueOf("Y");
System.out.println(y == a2); // true
}
}
多学一招:使用枚举类实现单例设计模式——只写一个枚举项(如
enum B { X; ... }),JVM 保证该实例唯一且线程安全,这是最简洁的单例写法。
6.3 枚举的常见应用场景
枚举的常见应用场景:用来表示一组信息,然后作为参数进行传输。
| 方案 | 做法 | 效果 |
|---|---|---|
| 定义一个个常量 | 用 public static final int 表示一组信息并作为参数传输 | 参数值不受约束,可读性差,维护麻烦 |
| 定义枚举 | 用 enum 表示一组信息并作为参数传输 | 代码可读性好,参数值得到了约束,对使用者更友好,建议使用 |
public class Constant {
public static final int DOWN = 1;
public static final int UP = 2;
public static final int HALF_UP = 3;
public static final int DELETE_LEFT = 4;
}
public enum Constant2 {
DOWN, UP, HALF_UP, DELETE_LEFT;
}
class Test {
public static void main(String[] args) {
// 常量方式:调用方可以传任意 int,参数值不受约束
System.out.println(handleData(3.9991, Constant.DOWN));
// 枚举方式:参数值被约束为枚举项,可读性更好
System.out.println(handleData(3.9991, Constant2.DOWN));
}
// 常量为参数:flag 是 int,随便传 999 也能编译通过
public static double handleData(double number, int flag) {
switch (flag) {
case Constant.DOWN:
number = Math.floor(number); // 向下取整,3.9991 == 3
break;
case Constant.UP:
number = Math.ceil(number); // 向上取整,3.1 == 4
break;
case Constant.HALF_UP:
number = Math.round(number); // 四舍五入,3.6 == 4
break;
case Constant.DELETE_LEFT:
number = (int) number; // 去掉小数部分
break;
}
return number;
}
// 枚举为参数:flag 只能是 Constant2 中列出的枚举项
public static double handleData(double number, Constant2 flag) {
switch (flag) {
case DOWN:
number = Math.floor(number);
break;
case UP:
number = Math.ceil(number);
break;
case HALF_UP:
number = Math.round(number);
break;
case DELETE_LEFT:
number = (int) number;
break;
}
return number;
}
}
示例中的性别选择就是同一思路:界面上”女生 / 男生”这类取值固定且需要作为参数传递的信息,用枚举表达比用数字或字符串更安全。
7. 泛型
7.1 认识泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量(如 <E>),称为泛型类、泛型接口、泛型方法,它们统称为泛型。
public class ArrayList<E> {
...
}
- 作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!这样可以避免强制类型转换,及其可能出现的异常。
- 本质:把具体的数据类型作为参数传给类型变量。
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
// 不用泛型:类型无法统一,取出来必须强制转换
ArrayList list = new ArrayList();
list.add("黑马");
list.add(true);
list.add(99.5);
for (int i = 0; i < list.size(); i++) {
Object ele = list.get(i);
String result = (String) ele; // 强制转换,类型不对就抛异常
System.out.println(result);
}
// 使用泛型:编译期就约束了元素类型
ArrayList<String> list2 = new ArrayList<>(); // JDK 1.7 开始后面类型可以不写
list2.add("黑马");
list2.add("深圳");
list2.add("Java");
for (int i = 0; i < list2.size(); i++) {
String ele = list2.get(i); // 不需要强制转换
System.out.println(ele);
}
}
}
7.2 泛型类
修饰符 class 类名<类型变量, 类型变量, ...> {
}
public class ArrayList<E> {
...
}
注意:类型变量建议用大写的英文字母,常用的有:
E、T、K、V等。
需求:模拟 ArrayList 集合,自定义一个 MyArrayList 泛型类。
import java.util.ArrayList;
// 泛型类
public class MyArrayList<E> {
private ArrayList list = new ArrayList();
public boolean add(E e) {
list.add(e);
return true;
}
public boolean remove(E e) {
return list.remove(e);
}
@Override
public String toString() {
return list.toString();
}
}
class Test {
public static void main(String[] args) {
// 需求:模拟 ArrayList 集合,自定义一个 MyArrayList 的泛型类。
MyArrayList<String> list = new MyArrayList<>();
list.add("张无忌");
list.add("赵敏");
list.add("周芷若");
list.add("小昭");
list.remove("周芷若");
System.out.println(list);
}
}
7.3 泛型接口
修饰符 interface 接口名<类型变量, 类型变量, ...> {
}
public interface A<E> {
...
}
需求:必须完成学生、老师数据的增删改查操作——CRUD 逻辑完全一样,只有数据类型不同,用泛型接口一次写清楚。
// 泛型接口
public interface Data<T> {
void add(T t);
void delete(T t);
void update(T t);
T getById(int id);
}
public class Student {
}
public class Teacher {
}
// 实现类一:把 T 固定成 Student
public class StudentData implements Data<Student> {
@Override
public void add(Student student) {
}
@Override
public void delete(Student student) {
}
@Override
public void update(Student student) {
}
@Override
public Student getById(int id) {
return null;
}
}
// 实现类二:把 T 固定成 Teacher,方法与 StudentData 完全一致
public class TeacherData implements Data<Teacher> {
@Override
public void add(Teacher teacher) {
}
// delete / update / getById 同 StudentData,只是类型换成 Teacher(略)
}
7.4 泛型方法
修饰符 <类型变量, 类型变量, ...> 返回值类型 方法名(形参列表) {
}
注意类型变量写在返回值类型之前,这是泛型方法与普通方法、以及”普通方法里出现了未定义类型变量”的分界。
public static <T> void test(T t) {
}
需求:接收任意对象数组。
public class Test {
public static void main(String[] args) {
String[] names = {"谢霆锋", "陈羽凡", "王宝强", "贾乃亮"};
String rs1 = printArray(names);
Student[] students = new Student[60];
Student rs2 = printArray(students);
}
public static <T> T printArray(T[] array) {
return array[0];
}
}
7.5 通配符与上下限
- 通配符:就是
?,可以在”使用泛型”的时候代表一切类型;ETKV是在”定义泛型”的时候使用。 - 泛型上限:
? extends Car,?能接收的必须是Car或者其子类。 - 泛型下限:
? super Car,?能接收的必须是Car或者其父类。
需求:所有汽车一起参与比赛。
import java.util.ArrayList;
public class Test2 {
public static void main(String[] args) {
ArrayList<TSL> tsls = new ArrayList<>();
tsls.add(new TSL());
tsls.add(new TSL());
go(tsls);
ArrayList<LX> lxes = new ArrayList<>();
lxes.add(new LX());
lxes.add(new LX());
go(lxes);
// ArrayList<Dog> dogs = new ArrayList<>();
// go(dogs); // 编译错误:Dog 不是 Car 的子类,不满足 ? extends Car
}
// 虽然 TSL 和 LX 是 Car 的子类,但 ArrayList<TSL>、ArrayList<LX> 与 ArrayList<Car> 没有关系。
// 所以这里要用通配符 ? 来表示"Car 及其子类的集合"。
public static void go(ArrayList<? extends Car> cars) {
}
}
class Car {
}
class TSL extends Car {
}
class LX extends Car {
}
class Dog {
}
7.6 泛型的擦除问题与注意事项
- 泛型是工作在编译阶段的,一旦程序编译成 class 文件,class 文件中就不存在泛型了,这就是泛型擦除。
- 泛型不能直接支持基本数据类型,只能支持对象类型(引用数据类型)——所以集合、泛型中要写
Integer而不是int。
课件说法:“class 文件中就不存在泛型了”;更准确的表述:编译后类型参数会被擦除为它的上界(没有上界就是
Object),但 class 文件仍保留泛型签名(Signature)元数据,反射可以读到(补充说明)。
8. 常用 API(一):Object、Objects、包装类与字符串工具
8.1 什么是 API
API(Application Programming Interface):应用程序编程接口。就是 Java 帮我们已经写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。
本节的知识块(课件目录顺序):
| 序号 | 内容 |
|---|---|
| 1 | API 概述、接下来课程介绍 |
| 2 | Object 类 |
| 3 | Objects |
| 4 | 包装类 |
| 5 | StringBuilder |
| 6 | StringJoiner |
学习建议(课件的比喻):学 API 像学英语——先认”一个一个的单词”(类名、方法名往往就是英语单词),再靠”语法知识”把它们组合起来。看得懂英文单词和方法签名,查 API 文档的效率会显著提高。
API 文档按包组织,常用类集中在 java.base 下的 java.lang、java.util、java.io 等包中。
8.2 Object 类
Object 类是 Java 中所有类的祖宗类,因此,Java 中所有类的对象都可以直接使用 Object 类中提供的一些方法。一个类没有写 extends 时,默认继承 Object。
本日重点掌握两个方法:
| 方法 | 基本作用 | 存在的意义 |
|---|---|---|
toString() | 返回对象的字符串形式 | 为了被子类重写,以便返回对象具体的内容 |
equals(Object o) | 默认比较两个对象的地址是否相等 | 为了被子类重写,以便子类自己定制比较规则(比如比较对象内容) |
关键理解:直接比较两个对象的地址是否相同完全可以用 == 替代 equals,所以 Object 提供的 equals 的意义就是留给子类重写。
public class Student {
private String name;
private int age;
private double score;
// ... 构造器与 getter/setter 省略 ...
/**
* 比较者:t1 ==> this
* 被比较者:t2 ==> o
*/
@Override
public boolean equals(Object o) {
// 1、判断两个对象是否是同一个对象在比较,如果是,直接返回 true
if (this == o) {
return true;
}
// 2、如果 o 是 null 返回 false;如果 o 不是学生类型也返回 false (Student != Pig)
if (o == null || this.getClass() != o.getClass()) {
return false;
}
// 3、说明 o 一定是学生类型而且不为 null,开始比较内容
Student student = (Student) o;
return this.age == student.age
&& Double.compare(this.score, student.score) == 0
&& Objects.equals(this.name, student.name);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
class Test {
public static void main(String[] args) {
Student s1 = new Student("小昭", 21, 100);
// toString 默认返回"地址形式"没意义,开发中输出对象更想看内容
System.out.println(s1.toString()); // 直接输出对象时 toString 可以不写
System.out.println(s1);
Student t1 = new Student("殷素素", 35, 98);
Student t2 = new Student("殷素素", 35, 98);
System.out.println(t1.equals(t2)); // true:重写后按内容比较
System.out.println(t1 == t2); // false:两个不同对象
}
}
(补充说明)
Object还提供hashCode()、getClass()、clone()等方法,本日作业选择题里出现过getClass()与”不属于Object的方法”(如trim())的判断。按字段重写equals()时通常应一并重写hashCode()。
课件自测页的口径:
toString的作用是返回对象的字符串形式;存在的意义是让子类重写,以便返回子类对象的内容。equals的作用是默认比较两个对象的地址是否相等;存在的意义是让子类重写,以便比较对象的内容是否相同。
8.3 Objects 工具类
Objects 是一个工具类,提供了很多操作对象的静态方法。
为什么要用 Objects 的 equals 方法,而不是用对象自己的 equals 方法来比较?——更安全。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
源码分析:先比引用(同一个对象或都为 null 直接返回 true),再确认 a != null 才调用 a.equals(b),因此主调对象为 null 时不会爆空指针异常。
import java.util.Objects;
public class Test {
public static void main(String[] args) {
Student t1 = null;
Student t2 = new Student("蜘蛛精", 300, 85.5);
// System.out.println(t1.equals(t2)); // 主调对象是 null,直接爆出空指针异常
System.out.println(Objects.equals(t1, t2)); // false:更安全、更可靠,推荐使用
System.out.println(Objects.isNull(t1)); // true
System.out.println(t1 == null); // true
System.out.println(Objects.nonNull(t1)); // false
System.out.println(t1 != null); // false
}
}
今后比较两个对象时,建议用
Objects的equals来判断。
8.4 包装类
为什么要用包装类? 为了”万物皆对象”,并且泛型和集合都不支持基本类型,只支持包装类。
包装类就是把基本类型的数据包装成对象。
| 基本数据类型 | 包装类 | 基本数据类型 | 包装类 |
|---|---|---|---|
byte | Byte | float | Float |
short | Short | double | Double |
int | Integer | char | Character |
long | Long | boolean | Boolean |
记忆法:8 种包装类中,int → Integer、char → Character,其他的都是首字母大写。
- 自动装箱:基本数据类型可以自动转换为包装类型。
- 自动拆箱:包装类型可以自动转换为基本数据类型。
public class Test {
public static void main(String[] args) {
int a = 12;
// 1、把基本类型通过包装类包装成对象(手动包装)
// Integer it1 = new Integer(a); // 构造器方式已过时
Integer it1 = Integer.valueOf(a);
// 2、自动装箱:基本类型的数据可以直接变成对象赋值给包装类
Integer it2 = 128;
Integer it3 = 128;
System.out.println(it2 == it3); // false:超出缓存范围,是两个不同对象
// 3、自动拆箱:包装类的对象可以直接赋值给基本数据类型
int it4 = it3;
System.out.println(it4);
// 功能一:包装类可以把基本类型的数据转换成字符串
int a1 = 23;
String rs = Integer.toString(a1); // "23"
System.out.println(rs + 1); // 231
System.out.println(a1 + "" + 1); // 231,更简单的写法
// 功能二:把字符串类型的数值转换成对应的基本数据类型(很有用!)
String ageStr = "23";
int age = Integer.parseInt(ageStr);
System.out.println(age + 1); // 24
String scoreStr = "99.5";
double score = Double.parseDouble(scoreStr);
System.out.println(score + 0.5); // 100.0
}
}
包装类的其他常见操作:
| 方向 | 常用方法 |
|---|---|
| 基本类型 → 字符串 | Integer.toString(int)、包装对象 .toString()、值 + "" |
| 字符串 → 基本类型 | Integer.parseInt(String)、Double.parseDouble(String)、Integer.valueOf(String) |
(补充说明)
Integer.valueOf()对 -128~127 之间的值会返回缓存对象,所以两个Integer都用==比较时,小值可能为true、大值为false。比较包装类的数值应使用equals()、Objects.equals()或先拆箱,不要依赖==。字符串格式非法时parseInt会抛NumberFormatException,拆箱null会抛NullPointerException。
8.5 StringBuilder
StringBuilder 代表可变字符串对象,相当于是一个容器,它里面装的字符串是可以改变的,就是用来操作字符串的。
好处:StringBuilder 比 String 更适合做字符串的修改操作,效率会更高,代码也会更简洁。
// 用 String 频繁拼接:每次都会产生新的字符串对象,慢
public class Test {
public static void main(String[] args) {
String s = "";
for (int i = 0; i < 1000000; i++) {
s = s + "abc";
}
System.out.println(s);
}
}
// 用 StringBuilder 拼接:始终是同一个容器在追加,快
public class Test {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
sb.append("abc");
}
System.out.println(sb);
}
}
结论:
- 对于字符串相关的操作,如频繁的拼接、修改等,建议用
StringBuilder,效率更高! - 注意:如果操作字符串较少,或者不需要操作,以及定义字符串变量,还是建议用
String。 - 原因:
String是不可变字符串,频繁操作字符串会产生很多无用对象,性能差;StringBuilder是内容可变的字符串,拼接字符串性能好、代码优雅。
StringBuffer 与 StringBuilder:StringBuffer 的用法与 StringBuilder 是一模一样的,但 StringBuilder 是线程不安全的,StringBuffer 是线程安全的。
8.6 StringJoiner
为什么学 StringJoiner? ——既要高效,又要实现更方便的拼接。
StringJoiner 是 JDK 8 开始才有的,跟 StringBuilder 一样,也是用来操作字符串的,也可以看成是一个容器,创建之后里面的内容是可变的。
好处:不仅能提高字符串的操作效率,并且在有些场景下使用它操作字符串,代码会更简洁(例如带分隔符、带前后缀的拼接)。
(补充说明)常用用法:
new StringJoiner(分隔符)或new StringJoiner(分隔符, 前缀, 后缀),通过add()追加元素,toString()得到结果;此时不需要自己判断”最后一个元素不加分隔符”。
小结:StringJoiner 是 JDK 8 出现的一个可变的、操作字符串的容器,用它拼接字符串时不仅高效,而且代码的编写更加方便。
8.7 综合案例:返回任意整型数组的内容
需求:设计一个方法,用于返回任意整型数组的内容,要求返回的数组内容格式如:[11, 22, 33]。
分析:
| 问题 | 结论 |
|---|---|
| 方法是否需要接收数据? | 需要接收整型数组 |
| 方法是否需要返回数据? | 需要返回拼接后的结果 |
| 方法内部怎么做? | 遍历数组的数据,把遍历到的数据都拼接起来,此时使用 StringBuilder 来完成拼接 |
public static String getArrayData(int[] arr) {
// 1、判断 arr 是否为 null
if (arr == null) {
return null;
}
// 2、arr 数组对象存在。例如 arr = [11, 22, 33]
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < arr.length; i++) {
if (i == arr.length - 1) {
sb.append(arr[i]); // 最后一个元素后面不加逗号
} else {
sb.append(arr[i]).append(", "); // 链式调用
}
}
sb.append("]");
return sb.toString();
}
9. 作业与练习
作业文档:day03-面向对象高级部分:匿名内部类,泛型,枚举/day03作业/day03作业.md
配套代码工程:day03-面向对象高级部分:匿名内部类,泛型,枚举/代码/day03-oop-code/(src/com/itheima 下按 d1_innerclass1 … d13_integer 分目录,与本节章节一一对应)
| 题型 | 要点 |
|---|---|
| 一、选择题(12 题) | Object 类的继承关系与常见方法;外部类与内部类成员相互访问规则;泛型定义语法(如 class Test4{ void <T> test(){} } 不正确);匿名内部类能否有构造方法;成员/局部内部类的访问与继承报错 |
| 二、今日单词 | enum;Object 类的方法名;JDK 八种包装类类名(注意 String 不是包装类) |
| 三、简答题 | 静态内部类的加载时机;枚举与静态常量的区别;泛型的定义方式;泛型注意事项;浅克隆与深克隆;包装类的作用 |
| 四、排错题(4 题) | 成员内部类中修改成员变量/局部变量;Integer 的 == 比较结果;匿名内部类中定义静态方法(注意 JDK 16 前后的版本差异);Inner extends Demo1 与 Demo1() 中 new Inner() 形成的递归构造 |
| 五、代码题(3 题) | Outer.method().show() 补全代码输出 HelloWorld(静态方法返回接口实现对象);用两种方式在 Test 中调用 methodA()(具名实现类 + 匿名内部类);【选做】用枚举优化”付款类型标号 → 中文含义”的 if...else |
两个值得推导的重点:
Outer.method().show()可拆成”静态方法返回一个Inter实现对象,再调用接口方法”,返回对象既可以是具名实现类,也可以是匿名内部类,返回类型写接口即可。- 排错题 4 中
Inner extends Demo1,而Demo1()又创建new Inner(),构造Inner会先调Demo1构造器,后者再次创建Inner,形成递归构造直至栈溢出——解决要打断对象创建环,而不是只加输出语句。
10. 复习要点
- 内部类是类的五大成分之一;四种形式是成员内部类、静态内部类、局部内部类(均为了解)、匿名内部类(重点)。
- 成员内部类无
static,属于外部类对象:new Outer().new Inner();可直接访问外部类的实例成员和静态成员;用外部类名.this取外部对象。JDK 16 起成员内部类允许定义静态成员。 - 静态内部类有
static,属于外部类本身:new Outer.Inner();只能直接访问外部类的静态成员,不能直接访问实例成员。 - 局部内部类定义在方法、代码块、构造器中,只在该作用域内可见;匿名内部类是特殊的局部内部类,格式
new 类或接口(参数值...){ 类体 },本质是一个子类并立即创建出对象。 - 匿名内部类的典型用法是作为参数传给方法(如把
Swimming实现交给go())。 - 枚举是一种特殊类:第一行只能是名称常量,每个名称记住一个枚举对象;构造器私有、类是
final、继承java.lang.Enum;编译器自动生成values()、valueOf()。 - 枚举用于表示一组信息并作为参数传输,比一堆
static final常量可读性更好、参数值受约束;用单个枚举项可实现最简洁的单例。 - 泛型的本质是把数据类型作为参数传给类型变量:泛型类
<E>写在类名后、泛型接口写在接口名后、泛型方法写在返回值类型前;?是使用时的通配符,? extends Car是上限、? super Car是下限。 - 泛型工作在编译阶段,编译后发生擦除;泛型不能直接用基本数据类型,要用包装类。注意”class 文件里完全不存在泛型”是课件的简化说法(签名元数据仍保留)。
Object是所有类的祖宗类;toString()、equals()的意义都是留给子类重写;比较对象优先用Objects.equals(a, b)(对null安全)。- 八种包装类:
int→Integer、char→Character,其余首字母大写;自动装箱/拆箱;包装类可做基本类型与字符串互转(parseInt/valueOf/toString);包装类数值比较用equals而非==。 - 频繁拼接/修改字符串用
StringBuilder(线程不安全)或StringBuffer(线程安全),用法一致;JDK 8 的StringJoiner在带分隔符/前后缀的场景更简洁;字符串变量本身仍用String。