1. 文章
  2. Java 进阶线
  3. 对象与类型设计

内部类、枚举与泛型:把对象和类型边界表达清楚

更新于 23 分钟阅读 第 3 / 15 站 #java#java-advanced#day03

掌握匿名内部类、付款枚举、泛型约束与对象相等规则。

我的学习 →

Java 进阶线 · 线路目录
本页导读

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 成员内部类中访问其他成员的特点

  1. 和前面学过的实例方法一样,成员内部类的实例方法中,同样可以直接访问外部类的实例成员、静态成员。
  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 枚举类的特点

  1. 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
  2. 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
  3. 枚举都是最终类,不可以被继承。
  4. 枚举类中,从第二行开始,可以定义类的其他各种成员。
  5. 编译器为枚举类新增了几个方法,并且枚举类都是继承 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 通配符与上下限

  • 通配符:就是 ?,可以在”使用泛型”的时候代表一切类型;E T K V 是在”定义泛型”的时候使用。
  • 泛型上限:? 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 泛型的擦除问题与注意事项

  1. 泛型是工作在编译阶段的,一旦程序编译成 class 文件,class 文件中就不存在泛型了,这就是泛型擦除。
  2. 泛型不能直接支持基本数据类型,只能支持对象类型(引用数据类型)——所以集合、泛型中要写 Integer 而不是 int。

课件说法:“class 文件中就不存在泛型了”;更准确的表述:编译后类型参数会被擦除为它的上界(没有上界就是 Object),但 class 文件仍保留泛型签名(Signature)元数据,反射可以读到(补充说明)。

8. 常用 API(一):Object、Objects、包装类与字符串工具

8.1 什么是 API

API(Application Programming Interface):应用程序编程接口。就是 Java 帮我们已经写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。

本节的知识块(课件目录顺序):

序号内容
1API 概述、接下来课程介绍
2Object 类
3Objects
4包装类
5StringBuilder
6StringJoiner

学习建议(课件的比喻):学 API 像学英语——先认”一个一个的单词”(类名、方法名往往就是英语单词),再靠”语法知识”把它们组合起来。看得懂英文单词和方法签名,查 API 文档的效率会显著提高。

JDK API 文档(黑马程序员版)——所有类的索引

API 文档按包组织,常用类集中在 java.base 下的 java.lang、java.util、java.io 等包中。

API 文档中的包结构(java.base → java → io / lang / net / util 等)

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()。

课件自测页的口径:

  1. toString 的作用是返回对象的字符串形式;存在的意义是让子类重写,以便返回子类对象的内容。
  2. 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 包装类

为什么要用包装类? 为了”万物皆对象”,并且泛型和集合都不支持基本类型,只支持包装类。

包装类就是把基本类型的数据包装成对象。

基本数据类型包装类基本数据类型包装类
byteBytefloatFloat
shortShortdoubleDouble
intIntegercharCharacter
longLongbooleanBoolean

记忆法: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);
    }
}

结论:

  1. 对于字符串相关的操作,如频繁的拼接、修改等,建议用 StringBuilder,效率更高!
  2. 注意:如果操作字符串较少,或者不需要操作,以及定义字符串变量,还是建议用 String。
  3. 原因: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. 复习要点

  1. 内部类是类的五大成分之一;四种形式是成员内部类、静态内部类、局部内部类(均为了解)、匿名内部类(重点)。
  2. 成员内部类无 static,属于外部类对象:new Outer().new Inner();可直接访问外部类的实例成员和静态成员;用 外部类名.this 取外部对象。JDK 16 起成员内部类允许定义静态成员。
  3. 静态内部类有 static,属于外部类本身:new Outer.Inner();只能直接访问外部类的静态成员,不能直接访问实例成员。
  4. 局部内部类定义在方法、代码块、构造器中,只在该作用域内可见;匿名内部类是特殊的局部内部类,格式 new 类或接口(参数值...){ 类体 },本质是一个子类并立即创建出对象。
  5. 匿名内部类的典型用法是作为参数传给方法(如把 Swimming 实现交给 go())。
  6. 枚举是一种特殊类:第一行只能是名称常量,每个名称记住一个枚举对象;构造器私有、类是 final、继承 java.lang.Enum;编译器自动生成 values()、valueOf()。
  7. 枚举用于表示一组信息并作为参数传输,比一堆 static final 常量可读性更好、参数值受约束;用单个枚举项可实现最简洁的单例。
  8. 泛型的本质是把数据类型作为参数传给类型变量:泛型类 <E> 写在类名后、泛型接口写在接口名后、泛型方法写在返回值类型前;? 是使用时的通配符,? extends Car 是上限、? super Car 是下限。
  9. 泛型工作在编译阶段,编译后发生擦除;泛型不能直接用基本数据类型,要用包装类。注意”class 文件里完全不存在泛型”是课件的简化说法(签名元数据仍保留)。
  10. Object 是所有类的祖宗类;toString()、equals() 的意义都是留给子类重写;比较对象优先用 Objects.equals(a, b)(对 null 安全)。
  11. 八种包装类:int→Integer、char→Character,其余首字母大写;自动装箱/拆箱;包装类可做基本类型与字符串互转(parseInt/valueOf/toString);包装类数值比较用 equals 而非 ==。
  12. 频繁拼接/修改字符串用 StringBuilder(线程不安全)或 StringBuffer(线程安全),用法一致;JDK 8 的 StringJoiner 在带分隔符/前后缀的场景更简洁;字符串变量本身仍用 String。