分享

6、Java常用工具类(下)

 夜猫速读 2022-05-05 发布于湖北

Java 包装类

本小节我们将学习 Java 的包装类,我们将了解到什么是包装类为什么需要包装类,Java 提供的包装类有哪些,各种包装类的常用方法和常量介绍,什么是装箱操作以及什么是拆箱操作等内容。

1. 什么是包装类

Java 有 8 种基本数据类型,Java 中的每个基本类型都被包装成了一个类,这些类被称为包装类。

包装类可以分为 3 类:NumberCharacterBoolean,包装类的架构图如下所示:

2. 为什么需要包装类

我们知道 Java 是面向对象的编程语言,但为了便于开发者上手,Java 沿用了 C 语言的基本数据类型,因此 Java 数据类型被分为了基本数据类型和引用数据类型。

对于简单的运算,开发者可以直接使用基本数据类型。但对于需要对象化交互的场景(例如将基本数据类型存入集合中),就需要将基本数据类型封装成 Java 对象,这是因为基本数据类型不具备对象的一些特征,没有对象的属性和方法,也不能使用面向对象的编程思想来组织代码。出于这个原因,包装类就产生了。

包装类就是一个类,因此它有属性、方法,可以对象化交互。

3. 基本数据类型与包装类

下表列出了基本数据类型对应的包装类。这些包装类都位于 java.lang 包下,因此使用包装类时,我们不需要手动引入。

基本数据类型对应的包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

除了 int 对应的包装类名称为 Integer 以及 char 对应的包装类名称 Character,其他 6 种数据类型对应的包装类,命名都为其基本数据类型的首字母的大写。

4. 包装类常用方法

4.1 Number 类

Number 类是所有数值类型包装类的父类,这里以其中一个子类 Integer 类为例,介绍其构造方法、常用方法以及常量。

4.1.1 构造方法

Integer 类提供两个构造方法:

  1. Integer(int value):以 int 型变量作为参数创建 Integer 对象;

  2. Integer(String s):以 String 型变量作为参数创建 Integer 对象。

实例如下:

// 以 int 型变量作为参数创建 Integer 对象
Integer num = new Integer(3);
// 以 String 型变量作为参数创建 Integer 对象
Integer num = new Integer("8");
代码块1234

4.1.2 常用方法

  • byte byteValue():以 byte 类型返回该 Integer 的值;

  • int compareTo(Integer anotherInteger):在数值上比较两个 Integer 对象。如果这两个值相等,则返回 0;如果调用对象的数值小于 anotherInteger 的数值,则返回负值;如果调用对象的数值大于 anotherInteger 的数值,则返回正值;

  • boolean equals(Object obj):比较此对象与指定对象是否相等;

  • int intValue():以 int 类型返回此 Integer 对象;

  • int shortValue():以 short 类型返回此 Integer 对象;

  • toString():返回一个表示该 Integer 值的 String 对象;

  • static Integer valueOf(String str):返回保存指定的 String 值的 Integer 对 象;

  • int parseInt(String str):返回包含在由 str 指定的字符串中的数字的等价整数值。

更多常用方法请翻阅官方文档

4.1.3 常用常量

  1. MAX_VALUE: 表示 int 型可取的最大值;

  2. MIN_VALUE: 表示 int 型可取的最小值;

  3. SIZE:表示以二进制补码形式表示 int 值的位数;

  4. TYPE: 表示基本类型 Class 实例。

这些常量的使用实例如下:

实例演示

public class WrapperClassDemo1 {
    public static void main(String[] args) {
        int maxValue = Integer.MAX_VALUE;
        int minValue = Integer.MIN_VALUE;
        int size = Integer.SIZE;
        System.out.println("int 类型可取的最大值" + maxValue);
        System.out.println("int 类型可取的最小值" + minValue);
        System.out.println("int 类型的二进制位数" + size);
    }
}
123456789101112

可查看在线运行效果

运行结果:

int 类型可取的最大值2147483647
int 类型可取的最小值-2147483648
int 类型的二进制位数32
代码块123

4.2 Character 类

Character 类在对象中包装一个基本类型为 char 的值。一个 Character 对象包含类型为 char 的单个字段。

4.2.1 构造方法

Character 类提供了一个构造方法:

Character(char value):很少使用。

4.2.2 常用方法

  • char charValue():返回此 Character 对象的值;

  • int compareTo(Character anotherCharacter):返回此 Character 对象的值,根据数字比较两个 Character 对象,若这两个对象相等则返回 0 ;

  • boolean equals(Object obj):将调用该方法的对象与指定的对象相比较;

  • char toUpperCase(char ch):将字符参数转换为大写;

  • char toLowerCase(char ch):将字符参数转换为小写;

  • String toString():返回一个表示指定 char 值的 String 对象;

  • char charValue():返回此 Character 对象的值;

  • boolean isUpperCase(char ch):判断指定字符是否是大写字符;

  • boolean isLowerCase(char ch):判断指定字符是否是小写字符。

更多常用方法请翻阅官方文档

4.3 Boolean 类

Boolean 类将基本类型为 boolean 的值包装在一个对象中。一个 Boolean 类型的对象只包含一个类型为 boolean 的字段。此外,此类还为 boolean 和 String 的相互转换提供了许多方法,并提供了处理 boolean 时非常有用的其他一些常量和方法。

4.3.1 构造方法

Boolean 类提供了如下两个构造方法:

  1. Boolean(boolean value):创建一个表示 value 参数的 boolean 对象(很少使用);

  2. Boolean(String s):以 String 变量作为参数,创建 boolean 对象。此时,如果传入的字符串不为 null,且忽略大小写后的内容等于 “true”,则生成 Boolean 对象值为 true,反之为 false。(很少使用)。

4.3.2 常用方法

  • boolean booleanValue():将 Boolean 对象的值以对应的 boolean 值返回;

  • boolean equals(Object obj):判断调用该方法的对象与 obj 是否相等,当且仅当参数不是 null,而且与调用该方法的对象一样都表示同一个 boolean 值的 Boolean 对象时, 才返回 true;

  • boolean parseBoolean(Sting):将字符串参数解析为 boolean 值;

  • String toString():返回表示该 boolean 值的 String 对象;

  • boolean valueOf(String s):返回一个用指定的字符串表示值的 boolean 值。

更多常用方法请翻阅官方文档

4.3.3 常用常量

  • TRUE:对应基值 true 的 Boolean 对象;

  • FALSR:对应基值 false 的 Boolean 对象;

  • TYPE:表示基本类型 Class 实例。

5. 装箱和拆箱

装箱就是基本数据类型向包装类转换;拆箱就是包装类向基本数据类型转换。装箱和拆箱又有自动和手动之分。

实现装箱的实例如下:

实例演示

public class WrapperClassDemo2 {
    public static void main(String[] args) {
        // 自动装箱
        int num1 = 19;
        Integer num2 = num1;
        System.out.println("num2=" + num2);
        // 手动装箱
        Integer num3 = new Integer(20);
        System.out.println("num3=" + num3);
    }
}
1234567891011121314

可查看在线运行效果

运行结果:

num2=19
num3=20
代码块12

自动装箱就是直接将一个基本数据类型的变量,赋值给对应包装类型的变量;手动装箱就是调用包装类的构造方法(在 Java14 中已经过时,不推荐这样的操作)。

实现拆箱的实例如下:

实例演示

public class WrapperClassDemo3 {
    public static void main(String[] args) {
        // 自动拆箱
        Integer num1 = 19;
        int num2 = num1;
        System.out.println("num2=" + num2);
        // 手动拆箱
        int num3 = num1.intValue();
        System.out.println("num3=" + num3);
    }
}
1234567891011121314

可查看在线运行效果

运行结果:

num2=19
num3=19
代码块12

自动拆箱就是直接将一个包装类型的变量,赋值给对应的基本数据类型变量;手动拆箱通过调用对应包装类下的 xxxValue() 方法来实现。

Java 枚举类

本小节我们将一起学习 Java 枚举类,将涉及到什么是枚举类为什么需要枚举类如何自定义枚举类以及如何使用枚举类Enum 类的常用方法等内容。理解为什么需要枚举类以及学会自定义枚举类是本小节学习的重点。

1. 什么是枚举类

在数学和计算机科学中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的技术。

枚举是一个被命名的整型常数的集合。枚举在生活中非常常见,列举如下:

  • 表示星期几:SUNDAYMONDAYTUESTDAYWEDNESDAYTHURSDAYFRIDAYSATURDAY就是一个枚举;

  • 性别:MALE(男)、FEMALE(女)也是一个枚举;

  • 订单的状态:PAIDED(已付款)、UNPAIDED(未付款)、FINISHED(已完成),CANCELED(已取消)。

知道了什么是枚举,我们就很容易理解什么是枚举类了,简单来说,枚举类就是一个可以表示枚举的类,当一个类的对象只有有限个、确定个的时候,我们就可以定义一个枚举类来存放这些对象。

2. 为什么需要枚举类

如果不使用枚举类,我们想在一个类中定义星期一到星期天,就可能需要在类中使用常量来表示,实例如下:

public class Weekday {
    public static final int MONDAY = 1;
    public static final int TUESDAY = 2;
    public static final int WEDNESDAY = 3;
    public static final int THURSDAY = 4;
    public static final int FRIDAY = 5;
    public static final int SATURDAY = 6;
    public static final int SUNDAY = 7;
}
代码块123456789

使用一组常量表示一个枚举的集合,存在一个问题,编译器无法检测每个值的范围是否合法,例如:

int day = 0; // 假设 day 的值为 0
if (day == Weekday.MON) {
    System.out.println("今天星期一");
}
代码块1234

显然,0 不在这些常量值所表示的范围(1~7)内,但编译器不会给出提示,这样的编码是非常不推荐的。

当我们在开发中需要定义一组常量的时候,建议使用枚举类。接下来我们就来看如何定义枚举类。

3. 如何自定义枚举类

自定义枚举类有两种方式:

  1. 在 Java 5.0 之前,需要通过普通 Java 类的“改装”来定义一个枚举类;

  2. 在 Java 5.0 之后,可以使用 enum关键字来定义枚举类。

下面我们分别来看下这两种定义枚举类的方式。

3.1 Java 5.0 之前自定义枚举类

在 Java 5.0之前,想要定义一个枚举类较为繁琐,通常需要以下几个步骤:

  1. 定义一个 Java 普通类作为枚举类,定义枚举类的属性,使用private final修饰;

  2. 该类不提供外部实例化操作,因此将构造方法设置为私有,并初始化属性;

  3. 在类内部,提供当前枚举类的多个对象 ,使用public static final修饰;

  4. 提供常用的gettersettertoString()方法。

下面我们定义一个用于表示性别的枚举类,并演示如何调用此枚举类,其具体实例如下:

/**
 * @author colorful@TaleLin
 */
public class EnumDemo1 {
    /**
     * 性别枚举类
     */
    static class Sex {
        // 定义常量
        private final String sexName;
        // 私有化构造器,不提供外部实例化
        private Sex(String sexName) {
            // 在构造器中为属性赋值
            this.sexName = sexName;
        }
        public static final Sex MALE = new Sex("男");
        public static final Sex FEMALE = new Sex("女");
        public static final Sex UNKNOWN = new Sex("保密");
        /**
         * getter
         */
        public String getSexName() {
            return sexName;
        }
        /**
         * 重写toString方法,方便外部打印调试
         */
        @Override
        public String toString() {
            return "Sex{" +
                    "sexName='" + sexName + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) {
        System.out.println(Sex.FEMALE.getSexName());
        System.out.println(Sex.MALE.getSexName());
        System.out.println(Sex.UNKNOWN.getSexName());
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

运行结果:

保密
代码块123

3.2 Java 5.0 之后自定义枚举类

在 Java 5.0 后,可以使用eunm关键字来定义一个枚举类,比较便捷,推荐大家使用这个方法来定义枚举类。

通常需要以下几个步骤:

  1. 使用enum关键字定义枚举类,这个类隐式继承自java.lang.Enum类;

  2. 在枚举类内部,提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾;

  3. 声明枚举类的属性和构造方法,在构造方法中为属性赋值;

  4. 提供 getter 方法,由于Enum类重写了 toString()方法,因此一般不需要我们自己来重写。

具体实例如下:

/**
 * @author colorful@TaleLin
 */
public class EnumDemo2 {
    public static void main(String[] args) {
        Sex male = Sex.MALE;
        // 打印 Sex 对象
        System.out.println(male);
        // 打印 getter方法的值
        System.out.println(male.getSexName());
        System.out.println(Sex.FEMALE.getSexName());
        System.out.println(Sex.UNKNOWN.getSexName());
    }
}
/**
 * 使用 enum 关键字定义枚举类,默认继承自 Enum 类
 */
enum Sex {
    // 1.提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾
    MALE("男"),
    FEMALE("女"),
    UNKNOWN("保密");
    /**
     * 2.声明枚举类的属性
     */
    private final String sexName;
    /**
     * 3.编写构造方法,为属性赋值
     */
    Sex(String sexName) {
        this.sexName = sexName;
    }
    /**
     * 4.提供getter
     */
    public String getSexName() {
        return sexName;
    }
}
代码块123456789101112131415161718192021222324252627282930313233343536373839404142434445

运行结果:

MALE
保密
代码块1234

4. Enum 类

java.lang.Enum类 是 Java 语言枚举类型的公共基类,我们使用enum关键字定义的枚举类,是隐式继承自Enum类的,下面我们来看一下Enum类的常用方法:

  • values():返回枚举类型的对象数组。改方法可以很方便的遍历所有的枚举值;

  • valueOf():可以把一个字符串转换为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”,如果不是,会抛出IllegalArguementException

  • toString():返回当前枚举类对象常量的名称。

这 3 个方法使用起来比较简单,因此我们写在一个实例中,代码如下:

/**
 * @author colorful@TaleLin
 */
public class EnumDemo3 {
    public static void main(String[] args) {
        Sex male = Sex.MALE;
        System.out.println("调用 toString() 方法:");
        System.out.println(male.toString());
        System.out.println("调用 values() 方法:");
        Sex[] values = Sex.values();
        for (Sex value : values) {
            System.out.println(value);
        }
        System.out.println("调用 valueOf() 方法:");
        Sex male1 = Sex.valueOf("MALE");
        System.out.println(male1);
    }
}
/**
 * 使用 enum 关键字定义枚举类,默认继承自 Enum 类
 */
enum Sex {
    // 1.提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾
    MALE("男"),
    FEMALE("女"),
    UNKNOWN("保密");
    /**
     * 2.声明枚举类的属性
     */
    private final String sexName;
    /**
     * 3.编写构造方法,为属性赋值
     */
    Sex(String sexName) {
        this.sexName = sexName;
    }
    // 提供 getter 和 setter
    public String getSexName() {
        return sexName;
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

运行结果:

调用 toString() 方法:
MALE
调用 values() 方法:
MALE
FEMALE
UNKNOWN
调用 valueOf() 方法:
MALE
代码块12345678

值得注意的是,当调用valuOf()方法时,我们传递的对象的“名字”,在枚举类中并不存在,此时会抛出运行时异常:IllegalArgumentException,实例如下:

/**
 * @author colorful@TaleLin
 */
public class EnumDemo3 {
    public static void main(String[] args) {
        System.out.println("调用 valueOf() 方法:");
        Sex male1 = Sex.valueOf("MALE1");
        System.out.println(male1);
    }
}
/**
 * 使用 enum 关键字定义枚举类,默认继承自 Enum 类
 */
enum Sex {
    // 1.提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾
    MALE("男"),
    FEMALE("女"),
    UNKNOWN("保密");
    /**
     * 2.声明枚举类的属性
     */
    private final String sexName;
    /**
     * 3.编写构造方法,为属性赋值
     */
    Sex(String sexName) {
        this.sexName = sexName;
    }
    // 提供 getter 和 setter
    public String getSexName() {
        return sexName;
    }
}
代码块12345678910111213141516171819202122232425262728293031323334353637383940

运行结果:

调用 valueOf() 方法:
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant Sex.MALE1
    at java.base/java.lang.Enum.valueOf(Enum.java:273)
    at Sex.valueOf(EnumDemo3.java:17)
    at EnumDemo3.main(EnumDemo3.java:8)
代码块12345

Java 集合

在前面的小节中,我们学习了数组,本小节学习的集合同样用于存放一组数据,我们将学习什么是集合集合的应用场景 ,在应用场景部分我们将对比 Java 数组与集合的区别,还将系统介绍 Java 集合的架构,也将结合实例来讲解集合的实际应用。

1. 什么是集合

在计算机科学中,集合是一组可变数量的数据项(也可能为 0 个)的组合,这些数据可能共享某些特征,需要以某种操作方式一起进行操作。

Java 中集合主要分为java.util.Collectionjava.util.Map两大接口。

下图描绘了 Java 集合的框架:

Tips: 图表最下方的ArrayListLinkedListHashSet以及HashMap都是常用实现类,本小节将介绍具体使用。

1.1 Collection

java.util.Collection接口的实现可用于存储 Java 对象。例如,夜猫编程的所有学生可以视为一个Collection

Collection又可以分为三个子接口,分别是:

  1. List:序列,必须按照顺序保存元素,因此它是有序的,允许重复;

  2. Queue:队列,按照排队规则来确定对象产生的顺序,有序,允许重复;

  3. Set:集,不能重复。

1.2 Map

java.util.Map接口的实现可用于表示“键”(key)和“值”(value)对象之间的映射。一个映射表示一组“键”对象,其中每一个“键”对象都映射到一个“值”对象。因此可以通过键来查找值。例如,夜猫编程的每一个学生都有他自己的账户积分,这个关联关系可以用Map来表示。

2. 集合的应用场景

2.1 数组与集合

在介绍集合的应用场景之前,我们先来看看数组和集合的对比。

我们知道数组和集合都用于存放一组数据,但数组的容量是固定大小的,而集合的容量是动态可变的;对于可存放的数据类型,数组既可以存放基本数据类型又可以存放引用数据类型,而集合只能存放引用数据类型,基本数据类型需要转换为对应的包装类才能存放到集合当中。

2.2 集合应用场景

  • 无法预测存储数据的数量:由于数组容量是固定大小,因此使用集合存储动态数量的数据更为合适;

  • 同时存储具有一对一关系的数据:例如存储夜猫编程学生的积分,为了方便检索对应学生的积分,可使用Map夜猫编程学生的uid和对应的积分进行一对一关联;

  • 数据去重:使用数组实现需要遍历,效率低,而Set集合本身就具有不能重复的特性;

  • 需要数据的增删:使用数组实现增删操作需要遍历、移动数组中元素,如果操作频繁会导致效率降低。

3. List 集合

3.1 概念和特性

List 是元素有序并且可以重复的集合,称之为序列。序列可以精确地控制每个元素的插入位置或删除某个位置的元素。通过前面的学习,我们知道ListCollection的一个子接口,它有两个主要实现类,分别为ArrayList(动态数组)和LinkedList(链表)。

3.2 ArrayList 实现类

ArrayList 可以理解为动态数组,它的容量可以动态增长。当添加元素时,如果发现容量已满,会自动扩容为原始大小的 1.5 倍。

3.2.1 构造方法

  • ArrayList():构造一个初始容量为 10 的空列表;

  • ArrayList(int initialCapacity):构造一个指定容量的空列表;

  • ArrayList(Collection<? extends E> c):构造一个包含指定集合元素的列表,其顺序由集合的迭代器返回。

在代码中,我们可以这样实例化ArrayList对象:

// 无参构造实例化,初始容量为10
List arrayList1 = new ArrayList();
// 实例化一个初始容量为20的空列表
List arrayList2 = new ArrayList(20);
// 实例化一个集合元素为 arrayList2 的列表(由于 arrayList2 为空列表,因此其实例化的对象也为空列表)
List arrayList3 = new ArrayList(arrayList2);
代码块123456

3.2.2 常用成员方法

  • void add(E e):将指定的元素追加到此列表的末尾;

  • void add(int index, E element):将指定的元素插入此列表中的指定位置;

  • E remove(int index):删除此列表中指定位置的元素;

  • boolean remove(Object o):如果存在指定元素,则从该列表中删除第一次出现的该元素;

  • void clear():从此列表中删除所有元素;

  • E set(int index, E element):用指定的元素替换此列表中指定位置的元素;

  • E get(int index):返回此列表中指定位置的元素;

  • boolean contains(Object o):如果此列表包含指定的元素,则返回 true,否则返回 false;

  • int size():返回该列表中元素的数量;

  • Object[] toArray():以正确的顺序(从第一个元素到最后一个元素)返回一个包含此列表中所有元素的数组。

更多成员方法请翻阅官方文档,下面我们将结合实例来介绍以上成员方法的使用。

3.3 实例

3.3.1 新增元素

请查看如下实例:

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo1 {
    public static void main(String[] args) {
        // 实例化一个空列表
        List arrayList = new ArrayList();
        for (int i = 0; i < 5; i ++) {
            // 将元素 i 追加到列表的末尾
            arrayList.add(i);
            // 打印列表内容
            System.out.println(arrayList);
        }
    }
}
代码块12345678910111213141516

运行结果:

[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]
代码块12345

代码中,首先实例化了一个ArrayList对象,然后使用 for 循环语句循环 5 次,每次都向arrayList对象中追加变量i,并打印列表内容,运行结果清晰的展示了每次新增元素的过程。

Tips:由于ArrayList的父类AbstractCollection重写了toString()方法,因此直接打印列表,可以直观地展示出列表中的元素。

3.3.2 泛型初识

Tips:泛型(Genericity)**将在下一小节详细介绍,此处我们只简要介绍一下泛型以及其使用方法。如果你比较了解泛型,可直接跳过此知识点。

如果你使用IDEA编写如上代码,将会有下图所示的 3 处黄色警告:

既然IDE有了警告,我们就尝试来解决一下,将鼠标光标放置到警告处,会提示“Unchecked call to 'add(E)’ as a member of raw type 'java.util.List’ ”,这是IDE泛型检查,可点击Try to generify 'ArrayListDemo1.java'按钮:

此时会出现一个Generify的弹窗,直接点击Refactor按钮:

代码变成了下图所示的样子,那 3 处警告被成功消除了:

我们观察到代码第 8 行的List类型后面多了一对尖括号“<>”,<>里面是 Java 的包装类型Integer,在ArrayList类型后面也多了一对尖括号,这里的<>中承载的就是 Java 的泛型的类型参数,它表示arrayList对象用于存放Integer类型的数据。这样的目的和好处这里不详细展开讨论,本小节我们只需知道这样做就可以消除IDEA的警告即可。

由于前面List已经指定了泛型的参数类型为Integer,后面的ArrayList就不需要再重复指定了。当然你也可以这样写(但是没必要):

List<Integer> arrayList = new ArrayList<Integer>();
代码块1

同理,如果你想向arrayList存放String类型的元素,只需将<Integer>改为<String>,我们再来看一个实例:

实例演示

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo2 {
    public static void main(String[] args) {
        // 实例化一个空列表
        List<String> arrayList = new ArrayList<>();
        // 将字符串元素 Hello 追加到此列表的末尾
        arrayList.add("Hello");
        // 将字符串元素 World 追加到此列表的末尾
        arrayList.add("World");
        // 打印列表
        System.out.println(arrayList);
        // 将字符串元素 Java 插入到此列表中的索引为 1 的位置
        arrayList.add(1, "Java");
        // 打印列表
        System.out.println(arrayList);
    }
}
12345678910111213141516171819

可查看在线运行效果

运行结果:

[Hello, World]
[Hello, Java, World]
代码块12

代码中,首先实例化了一个ArrayList的对象,调用了两次add(E e)方法,依次向列表尾部插入了HelloWorld元素,列表中元素为[Hello, World],此时调用add(int index, E element)方法,将字符串元素 Java 插入到此列表中的索引为 1 的位置,因此列表中的元素为[Hello, Java, World]

3.3.3 删除元素

请查看如下实例:

实例演示

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo3 {
    public static void main(String[] args) {
        // 实例化一个空列表
        List<String> arrayList = new ArrayList<>();
        // 将字符串元素 Hello 追加到此列表的末尾
        arrayList.add("Hello");
        // 将字符串元素 World 追加到此列表的末尾
        arrayList.add("World");
        // 将字符串元素 Hello 追加到此列表的末尾
        arrayList.add("Hello");
        // 将字符串元素 Java 追加到此列表的末尾
        arrayList.add("Java");
        // 打印列表
        System.out.println(arrayList);
        // 删除此列表中索引位置为 3 的元素
        arrayList.remove(3);
        // 打印列表
        System.out.println(arrayList);
        // 删除此列表中第一次出现的 Hello 元素
        arrayList.remove("Hello");
        System.out.println(arrayList);
    }
}
1234567891011121314151617181920212223242526272829

可查看在线运行效果

运行结果:

[Hello, World, Hello, Java]
[Hello, World, Hello]
[World, Hello]
代码块123

代码中,我们首先添加了 4 个字符串元素,列表内容为[Hello, World, Hello, Java],然后调用remove(int index)方法删除了索引位置为 3 的元素(即Java),此时列表内容为[Hello, World, Hello] ,再次调用remove(Object o)方法,删除了列表中第一次出现的Hello元素,此时列表内容为[World, Hello]

3.3.4 修改元素

可使用set()方法修改列表中元素,实例如下:

实例演示

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo4 {
    public static void main(String[] args) {
        // 实例化一个空列表
        List<String> arrayList = new ArrayList<>();
        arrayList.add("Hello");
        // 将字符串元素 World 追加到此列表的末尾
        arrayList.add("World");
        // 打印列表
        System.out.println(arrayList);
        // 用字符串元素 Hello 替换此列表中索引位置为 1 的元素
        arrayList.set(1, "Java");
        System.out.println(arrayList);
    }
}
123456789101112131415161718

可查看在线运行效果

运行结果:

[Hello, World]
[Hello, Java]
代码块12

3.3.5 查询元素

可使用get()方法来获取列表中元素,实例如下:

实例演示

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo5 {
    public static void main(String[] args) {
        // 实例化一个空列表
        List<String> arrayList = new ArrayList<String>();
        arrayList.add("Hello");
        arrayList.add("Immoc");
        for (int i = 0; i < arrayList.size(); i ++) {
            System.out.println("索引位置" + i + "的元素为"  + arrayList.get(i));
        }
    }
}
123456789101112131415

可查看在线运行效果

运行结果:

索引位置0的元素为Hello
索引位置1的元素为Immoc
代码块12

我们在使用for循环遍历列表的时候,让限定条件为i < arrayList.size();size()方法可获取该列表中元素的数量。

3.2.7 自定义类的常用操作

请查看如下实例:

实例演示

import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo6 {
    static class ImoocStudent {
        private String nickname;
        
        private String position;
        public ImoocStudent() {
        }
        
        public ImoocStudent(String nickname, String position) {
            this.setNickname(nickname);
            this.setPosition(position);
        }
        public String getNickname() {
            return nickname;
        }
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
        public String getPosition() {
            return position;
        }
        public void setPosition(String position) {
            this.position = position;
        }
        @Override
        public String toString() {
            return "ImoocStudent{" +
                    "nickname='" + nickname + '\'' +
                    ", position='" + position + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) {
        // 实例化一个空列表
        List<ImoocStudent> arrayList = new ArrayList<>();
        // 实例化3个夜猫编程学生对象
        ImoocStudent imoocStudent1 = new ImoocStudent("Colorful", "服务端工程师");
        ImoocStudent imoocStudent2 = new ImoocStudent("Lillian", "客户端工程师");
        ImoocStudent imoocStudent3 = new ImoocStudent("小斧", "架构师");
        // 新增元素
        arrayList.add(imoocStudent1);
        arrayList.add(imoocStudent2);
        arrayList.add(imoocStudent3);
        System.out.println(arrayList);
        // 删除元素
        arrayList.remove(imoocStudent2);
        System.out.println("删除 imoocStudent2 后:arrayList 内容为:" + arrayList);
        arrayList.remove(1);
        System.out.println("删除列表中索引位置为 1 的元素后,arrayList 内容为:" + arrayList);
        // 实例化一个新的夜猫编程
学生对象
        ImoocStudent imoocStudent4 = new ImoocStudent("小李", "UI设计师");
        // 修改元素
        arrayList.set(0, imoocStudent4);
        System.out.println("修改后:arrayList 内容为" + imoocStudent4);
        // 查询元素,将 get() 方法得到的 Object 类型强制转换为 ImoocStudent 类型
        ImoocStudent student = arrayList.get(0);
        System.out.println("索引位置 0 的学生的昵称为:" + student.getNickname());
        System.out.println("索引位置 0 的学生的职位为:" + student.getPosition());
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071

可查看在线运行效果

运行结果:

[ImoocStudent{nickname='Colorful', position='服务端工程师'}, ImoocStudent{nickname='Lillian', position='客户端工程师'}, ImoocStudent{nickname='小斧', position='架构师'}]
删除 imoocStudent2 后:arrayList 内容为:[ImoocStudent{nickname='Colorful', position='服务端工程师'}, ImoocStudent{nickname='小斧', position='架构师'}]
删除列表中索引位置为 1 的元素后,arrayList 内容为:[ImoocStudent{nickname='Colorful', position='服务端工程师'}]
修改后:arrayList 内容为ImoocStudent{nickname='小李', position='UI设计师'}
索引位置 0 的学生的昵称为:小李
索引位置 0 的学生的职位为:UI设计师
代码块123456

为了方便演示,我们定义了一个静态内部类ImoocStudent,它有两个属性nicknameposition,定义了属性的gettersetter,并重写了toString()方法。在main()方法中,我们实现了自定义类在ArrayList中的增删改查。

3.4 LinkedList 实现类

LinkedList 是一个以双向链表实现的List。和ArrayList一样,也按照索引位置排序,但它的元素是双向连接的,因此顺序访问的效率非常高,而随机访问的效率比较低。

3.4.1 构造方法

  • LinkedList():构造一个空列表;

  • LinkedList(Collection<? extends E> c):构造一个包含指定集合元素的列表,其顺序由集合的迭代器返回。

3.4.2 常用成员方法

  • void add(E e):将指定的元素追加到此列表的末尾;

  • void add(int index, E element):将指定的元素插入此列表中的指定位置;

  • void addFirst(E e):将指定的元素插入此列表的开头;

  • vod addLast(E e):将指定的元素添加到此列表的结尾;

  • E remove(int index):删除此列表中指定位置的元素;

  • boolean remove(Object o):如果存在指定元素,则从该列表中删除第一次出现的该元素;

  • void clear():从此列表中删除所有元素;

  • E set(int index, E element):用指定的元素替换此列表中指定位置的元素;

  • E get(int index):返回此列表中指定位置的元素;

  • E getFirst():返回此列表的第一个元素;

  • E getLast():返回此列表的最后一个元素;

  • boolean contains(Object o):如果此列表包含指定的元素,则返回 true,否则返回 false;

  • int size():返回该列表中元素的数量;

  • Object[] toArray():以正确的顺序(从第一个元素到最后一个元素)返回一个包含此列表中所有元素的数组。

更多成员方法请翻阅官方文档,对于成员方法的使用,与ArrayList大同小异,这里不再赘述。

4. Set 集合

4.1 概念和特性

Set是元素无序并且不可以重复的集合,我们称之为集。SetCollection的一个子接口,它的主要实现类有:HashSetTreeSetLinkedHashSetEnumSet等,下面我们将详细介绍最常用的HashSet实现类。

4.2 HashSet 实现类

HashSet类依赖于哈希表(实际上是HashMap实例,下面将会介绍)。HashSet中的元素是无序的、散列的。

4.2.1 构造方法

  • HashSet():构造一个新的空集;默认的初始容量为 16(最常用),负载系数为 0.75;

  • HashSet(int initialCapacity):构造一个新的空集; 具有指定的初始容量,负载系数为 0.75;

  • HashSet(int initialCapacity, float loadFactor):构造一个新的空集; 支持的 HashMap 实例具有指定的初始容量和指定的负载系数;

  • HashSet(Collection<? extends E> c):构造一个新集合,其中包含指定集合中的元素。

4.2.2 常用成员方法

HashSet的常用成员方法如下:

  • boolean add(E e):如果指定的元素尚不存在,则将其添加到该集合中;

  • boolean contains(Object o):如果此集合包含指定的元素,则返回 true,否则返回 false;

  • boolean isEmpty():如果此集合不包含任何元素,则返回 true,否则返回 false;

  • Iterator<E> iterator():返回此集合中元素的迭代器;

  • boolean remove(Object o):从该集合中删除指定的元素(如果存在);

  • int size():返回此集合中的元素数量。

更多成员方法请翻阅官方文档,下面我们将结合实例来介绍以上成员方法的使用。

4.3 实例

4.3.1 新增元素

可使用add()方法向集中添加元素,实例如下:

实例演示

import java.util.HashSet;
import java.util.Set;
public class HashSetDemo1 {
    public static void main(String[] args) {
        // 实例化一个新的空集
        Set<String> hashSet = new HashSet<String>();
        // 向 hashSet 集中依次添加元素:Python、Java、PHP、TypeScript、Python
        hashSet.add("Python");
        hashSet.add("Java");
        hashSet.add("PHP");
        hashSet.add("TypeScript");
        hashSet.add("Python");
        // 打印 hashSet 的内容
        System.out.println("hashSet中的内容为:" + hashSet);
    }
}
1234567891011121314151617

可查看在线运行效果

运行结果:

hashSet中的内容为:[TypeScript, Java, PHP, Python]
代码块1

在实例中,我们先后向hashSet中添加了两次Python元素,由于集的元素不可重复特性,因此集中只允许出现一个Python元素。我们还观察到,打印结果的元素顺序和我们添加的顺序是不同的,这验证了集的无序特性

Tips: 由于HashSet的父类AbstractCollection重写了toString()方法,因此直接打印集,可以直观地展示出集中的元素。

4.3.2 删除元素

可使用remove()方法删除集中元素,实例如下:

实例演示

import java.util.HashSet;
import java.util.Set;
public class HashSetDemo2 {
    public static void main(String[] args) {
        // 实例化一个新的空集
        Set<String> hashSet = new HashSet<>();
        // 向 hashSet 集中依次添加元素:Python、Java
        hashSet.add("Python");
        hashSet.add("Java");
        // 打印 hashSet 的内容
        System.out.println(hashSet);
        // 删除 hashSet 中的 Python 元素
        hashSet.remove("Python");
        // 打印 hashSet 的内容
        System.out.println("删除 Python 元素后,hashSet中的内容为:" + hashSet);
    }
}
123456789101112131415161718

可查看在线运行效果

运行结果:

[Java, Python]
删除 Python 元素后,hashSet中的内容为:[Java]
代码块12

4.3.3 查询元素

我们知道了ArrayList 通过 get方法来查询元素,但HashSet没有提供类似的get方法来查询元素。

这里我们介绍一个迭代器(Iterator)接口,所有的Collection都实现了Iterator接口,它可以以统一的方式对各种集合元素进行遍历。我们来看下Iterator接口的常用方法:

  • hasNaxt() 方法检测集合中是否还有下一个元素;

  • next()方法返回集合中的下一个元素;

  • iterator():返回此集合中元素的迭代器。

实例如下:

实例演示

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetDemo3 {
    public static void main(String[] args) {
        // 实例化一个新的空集
        Set<String> hashSet = new HashSet<String>();
        // 向 hashSet 集中依次添加元素:Python、Java、PHP
        hashSet.add("Python");
        hashSet.add("Java");
        hashSet.add("PHP");
        // 打印 hashSet 的内容
        System.out.println(hashSet);
        // 获取 hashSet 中元素的迭代器
        Iterator<String> iterator = hashSet.iterator();
        System.out.println("迭代器的遍历结果为:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}
1234567891011121314151617181920212223

可查看在线运行效果

运行结果:

[Java, PHP, Python]
迭代器的遍历结果为:
Java
PHP
Python
代码块12345

4.3.4 自定义类的常用操作

请查看如下实例:

实例演示

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetDemo4 {
    /**
     * 静态内部类:夜猫编程学生
     */
    static class ImoocStudent {
        private String nickname;
        private String position;
        public ImoocStudent() {
        }
        public ImoocStudent(String nickname, String position) {
            this.setNickname(nickname);
            this.setPosition(position);
        }
        public String getNickname() {
            return nickname;
        }
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
        public String getPosition() {
            return position;
        }
        public void setPosition(String position) {
            this.position = position;
        }
        @Override
        public String toString() {
            return "ImoocStudent{" +
                    "nickname='" + nickname + '\'' +
                    ", position='" + position + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) {
        Set<ImoocStudent> hashSet = new HashSet<>();
        // 实例化3个夜猫编程学生对象
        ImoocStudent imoocStudent1 = new ImoocStudent("Colorful", "服务端工程师");
        ImoocStudent imoocStudent2 = new ImoocStudent("Lillian", "客户端工程师");
        ImoocStudent imoocStudent3 = new ImoocStudent("小斧", "架构师");
        // 新增元素
        hashSet.add(imoocStudent1);
        hashSet.add(imoocStudent2);
        hashSet.add(imoocStudent3);
        // 使用Iterator遍历hashSet
        Iterator<ImoocStudent> iterator = hashSet.iterator();
        System.out.println("迭代器的遍历结果为:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        // 查找并删除
        if (hashSet.contains(imoocStudent1)) {
            hashSet.remove(imoocStudent1);
        }
        System.out.println("删除nickname为Colorful的对象后,集合元素为:");
        System.out.println(hashSet);
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071

可查看在线运行效果

运行结果:

迭代器的遍历结果为:
ImoocStudent{nickname='Lillian', position='客户端工程师'}
ImoocStudent{nickname='Colorful', position='服务端工程师'}
ImoocStudent{nickname='小斧', position='架构师'}
删除nickname为Colorful的对象后,集合元素为:
[ImoocStudent{nickname='Lillian', position='客户端工程师'}, ImoocStudent{nickname='Colorful', position='服务端工程师'}, ImoocStudent{nickname='小斧', position='架构师'}]
代码块123456

为了方便演示,我们定义了一个静态内部类ImoocStudent,它有两个属性nicknameposition,定义了属性的gettersetter,并重写了toString()方法。在main()方法中,我们实现了自定义类在HashSet中的增删改查,使用迭代器可以遍历元素。

5. Map 集合

5.1 概念和特性

我们已经知道Map是以键值对(key-value)的形式存储的对象之间的映射,key-value是以java.util.Map.Entry类型的对象实例存在。

可以使用键来查找值,一个映射中不能包含重复的键,但值是可以重复的。每个键最多只能映射到一个值。

5.2 HashMap 实现类

HashMapjava.util.Map接口最常用的一个实现类,前面所学的HashSet底层就是通过HashMap来实现的,HashMap允许使用null键和null值。

5.2.1 构造方法

  • HashMap():构造一个新的空映射;默认的初始容量为 16(最常用),负载系数为 0.75;

  • HashMap(int initialCapacity):构造一个新的空映射; 具有指定的初始容量,负载系数为 0.75;

  • HashMap(int initialCapacity, float loadFactor):构造一个新的空映射; 支持的 HashMap 实例具有指定的初始容量和指定的负载系数;

  • HashSet(Map<? extends K, ? extends V> m):构造一个新映射,其中包含指定映射相同。

5.2.2 常用成员方法

  • void clear():从该映射中删除所有映射;

  • Set<Map, Entry<K, V>> entrySet:返回此映射中包含的映射的集合;

  • V get(Object key):返回指定键映射到的值,如果该映射不包含键的映射,则返回 null;

  • Set<K> keySet:返回此映射中包含的的结合;

  • V put(K key, V value):将指定值与此映射中指定键关联;

  • V remove(Object key):如果存在,则从此映射中删除指定键的映射。

  • Collection<V> values:返回此映射中包含的集合。

5.3 实例

下面我们使用 HashMap 来实现一个英汉字典的例子。

实例演示

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class HashMapDemo1 {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        // 添加数据
        map.put("English", "英语");
        map.put("Chinese", "汉语");
        map.put("Java", "咖啡");
        // 打印 map
        System.out.println(map);
        // 删除 key 为 Java 的数据
        map.remove("Chinese");
        System.out.println("删除键为Chinese的映射后,map内容为:");
        // 打印 map
        System.out.println(map);
        // 修改元素:
        map.put("Java", "一种编程语言");
        System.out.println("修改键为Java的值后,Java=" + map.get("Java"));
        // 遍历map
        System.out.println("通过遍历entrySet方法得到 key-value 映射:");
        Set<Entry<String, String>> entries = map.entrySet();
        for (Entry<String, String> entry: entries) {
            System.out.println(entry.getKey() + " - " + entry.getValue());
        }
        // 查找集合中键为 English 对应的值
        Set<String> keySet = map.keySet();
        for (String key: keySet) {
            if (key.equals("English")) {
                System.out.println("English 键对应的值为:" + map.get(key));
                break;
            }
        }
    }
}
123456789101112131415161718192021222324252627282930313233343536373839

可查看在线运行效果

运行结果:

{English=英语, Java=咖啡, Chinese=汉语}
删除键为Chinese的映射后,map内容为:
{English=英语, Java=咖啡}
修改键为Java的值后,Java=一种编程语言
通过遍历entrySet方法得到 key-value 映射:
English - 英语
Java - 一种编程语言
English 键对应的值为:英语
代码块12345678

实例中,Map 的 key 是字符串类型,value 也是字符串类型。值得注意的是,我们在创建HashMap的时候,在Map类型的后面有一个<String, String>,分别表示映射中将要存放的 key 和 value 的类型都为 String 类型。在遍历映射的时候,我们调用了entrySet方法,它返回了此映射中包含的映射的集合。通过键查找值,我们可以调用keySet方法来获取映射中的键的集合,并且遍历这个集合即可找到对应键,通过键就可以获取值了。

Java 泛型

本小节我们将学习 Java5 以后出现的一个特性:泛型(Generics。通过本小节的学习,你将了解到什么是泛型为什么需要泛型,如何使用泛型,如何自定义泛型,类型通配符等知识。

1. 什么是泛型

泛型不只是 Java 语言所特有的特性,泛型是程序设计语言的一种特性。允许程序员在强类型的程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须做出声明。

我们在上一小节已经了解到,Java 中的集合类是支持泛型的,它在代码中是这个样子的:

代码中的<Integer>就是泛型,我们把类型像参数一样传递,尖括号中间就是数据类型,我们可以称之为实际类型参数,这里实际类型参数的数据类型只能为引用数据类型。

那么为什么需要泛型呢?我们马上就见分晓。

2. 为什么需要泛型

上一节中,我们在使用ArrayList实现类的时候,如果没有指定泛型,IDEA会给出警告,代码似乎也是可以顺利运行的。请看如下实例:

import java.util.ArrayList;
public class GenericsDemo1 {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add("Hello");
        String str = (String) arrayList.get(0);
        System.out.println("str=" + str);
    }
}
代码块123456789101112

运行结果:

str=Hello
代码块1

虽然运行时没有发生任何异常,但这样做有两个缺点:

  1. 需要强制类型转换: 由于ArrayList内部就是一个Object[]数组,在get()元素的时候,返回的是Object类型,所以在ArrayList外获取该对象,需要强制类型转换。其它的CollectionMap如果不使用泛型,也存在这个问题;

  2. 可向集合中添加任意类型的对象,存在类型不安全风险。例如如下代码中,我们向列表中既添加了Integer类型,又添加了String类型:

import java.util.ArrayList;
public class GenericsDemo2 {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("Hello");
        String str = (String) arrayList.get(0);
        System.out.println("element=" + str);
    }
}
代码块1234567891011

运行结果:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
    at GenericsDemo2.main(GenericsDemo2.java:8)
代码块12

由于我们的“疏忽”,列表第 1 个元素实际上是整型,但被我们强制转换为字符串类型,这是行不通的,因此会抛出ClassCastException异常。

使用泛型可以解决这些问题。泛型有如下优点:

  1. 可以减少类型转换的次数,代码更加简洁;

  2. 程序更加健壮:只要编译期没有警告,运行期就不会抛出ClassCastException异常;

  3. 提高了代码的可读性:编写集合的时候,就限定了集合中能存放的类型。

3. 如何使用泛型

3.1 泛型使用

在代码中,这样使用泛型:

List<String> list = new ArrayList<String>();
// Java 7 及以后的版本中,构造方法中可以省略泛型类型:
List<String> list = new ArrayList<>();
代码块123

要注意的是,变量声明的类型必须与传递给实际对象的类型保持一致,下面是错误的例子:

List<Object> list = new ArrayList<String>();
List<Number> numbers = new ArrayList(Integer);
代码块12

3.2 自定义泛型类

3.2.1 Java 源码中泛型的定义

在自定义泛型类之前,我们来看下java.util.ArrayList是如何定义的:

类名后面的<E>就是泛型的定义,E不是 Java 中的一个具体的类型,它是 Java 泛型的通配符(注意是大写的,实际上就是Element的含义),可将其理解为一个占位符,将其定义在类上,使用时才确定类型。此处的命名不受限制,但最好有一定含义,例如java.lang.HashMap的泛型定义为HashMap<K,V>K表示KeyV表示Value

3.2.2 自定义泛型类实例1

下面我们来自定义一个泛型类,自定义泛型按照约定俗成可以叫<T>,具有Type的含义,实例如下:

实例演示

public class NumberGeneric<T> { // 把泛型定义在类上
    private T number; // 定义在类上的泛型,在类内部可以使用
    public T getNumber() {
        return number;
    }
    public void setNumber(T number) {
        this.number = number;
    }
    public static void main(String[] args) {
        // 实例化对象,指定元素类型为整型
        NumberGeneric<Integer> integerNumberGeneric = new NumberGeneric<>();
        // 分别调用set、get方法
        integerNumberGeneric.setNumber(123);
        System.out.println("integerNumber=" + integerNumberGeneric.getNumber());
        // 实例化对象,指定元素类型为长整型
        NumberGeneric<Long> longNumberGeneric = new NumberGeneric<>();
        // 分别调用set、get方法
        longNumberGeneric.setNumber(20L);
        System.out.println("longNumber=" + longNumberGeneric.getNumber());
        // 实例化对象,指定元素类型为双精度浮点型
        NumberGeneric<Double> doubleNumberGeneric = new NumberGeneric<>();
        // 分别调用set、get方法
        doubleNumberGeneric.setNumber(4000.0);
        System.out.println("doubleNumber=" + doubleNumberGeneric.getNumber());
    }
}
123456789101112131415161718192021222324252627282930313233

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

integerNumber=123
longNumber=20
doubleNumber=4000.0
代码块123

我们在类的定义处也定义了泛型:NumberGeneric<T>;在类内部定义了一个T类型的number变量,并且为其添加了settergetter方法。

对于泛型类的使用也很简单,在主方法中,创建对象的时候指定T的类型分别为IntegerLongDouble,类就可以自动转换成对应的类型了。

3.2.3 自定义泛型类实例2

上面我们知道了如何定义含有单个泛型的类,那么对于含有多个泛型的类,如何定义呢?

我们可以看一下HashMap类是如何定义的。如下是 Java 源码的截图:

参照HashMap<K,V>类的定义,下面我们来看看如何定义含有两个泛型的类,实例如下:

实例演示

public class KeyValueGeneric<K,V> { // 把两个泛型K、V定义在类上
    /**
     * 类型为K的key属性
     */
    private K key;
    /**
     * 类型为V的value属性
     */
    private V value;
    public K getKey() {
        return key;
    }
    public void setKey(K key) {
        this.key = key;
    }
    public V getValue() {
        return value;
    }
    public void setValue(V value) {
        this.value = value;
    }
    public static void main(String[] args) {
        // 实例化对象,分别指定元素类型为整型、长整型
        KeyValueGeneric<Integer, Long> integerLongKeyValueGeneric = new KeyValueGeneric<>();
        // 调用setter、getter方法
        integerLongKeyValueGeneric.setKey(200);
        integerLongKeyValueGeneric.setValue(300L);
        System.out.println("key=" + integerLongKeyValueGeneric.getKey());
        System.out.println("value=" + integerLongKeyValueGeneric.getValue());
        // 实例化对象,分别指定元素类型为浮点型、字符串类型
        KeyValueGeneric<Float, String> floatStringKeyValueGeneric = new KeyValueGeneric<>();
        // 调用setter、getter方法
        floatStringKeyValueGeneric.setKey(0.5f);
        floatStringKeyValueGeneric.setValue("零点五");
        System.out.println("key=" + floatStringKeyValueGeneric.getKey());
        System.out.println("value=" + floatStringKeyValueGeneric.getValue());
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

key=200
value=300
key=0.5
value=零点五
代码块1234

3.3 自定义泛型方法

前面我们知道了如何定义泛型类,在类上定义的泛型,在方法中也可以使用。下面我们来看一下如何自定义泛型方法。

泛型方法不一定写在泛型类当中。当类的调用者总是关心类中的某个泛型方法,不关心其他属性,这个时候就没必要再整个类上定义泛型了。

请查看如下实例:

实例演示

public class GenericMethod {
    /**
     * 泛型方法show
     * @param t 要打印的参数
     * @param <T> T
     */
    public <T> void show(T t) {
        System.out.println(t);
    }
    public static void main(String[] args) {
        // 实例化对象
        GenericMethod genericMethod = new GenericMethod();
        // 调用泛型方法show,传入不同类型的参数
        genericMethod.show("Java");
        genericMethod.show(222);
        genericMethod.show(222.0);
        genericMethod.show(222L);
    }
}
123456789101112131415161718192021

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

Java
222
222.0
222
代码块1234

实例中,使用<T>来定义show方法的泛型,它接收一个泛型的参数变量并在方法体打印;调用泛型方法也很简单,在主方法中实例化对象,调用对象下的泛型方法,可传入不同类型的参数。

4. 泛型类的子类

泛型类也是一个 Java 类,它也具有继承的特性。

泛型类的继承可分为两种情况:

  1. 子类明确泛型类的类型参数变量;

  2. 子类不明确泛型类的类型参数变量。

下面我们来分别看一下这两种情况。

4.1 明确类型参数变量

例如,有一个泛型接口:

public interface GenericInterface<T> { // 在接口上定义泛型
    void show(T t);
}
代码块123

泛型接口的实现类如下:

public class GenericInterfaceImpl implements GenericInterface<String> { // 明确泛型类型为String类型
    @Override
    public void show(String s) {
        System.out.println(s);
    }
}
代码块123456

子类实现明确了泛型的参数变量为String类型。因此方法show()的重写也将T替换为了String类型。

4.2 不明确类型参数变量

当实现类不确定泛型类的参数变量时,实现类需要定义类型参数变量,调用者使用子类时,也需要传递类型参数变量。

如下是GenericInterface接口的另一个实现类:

public class GenericInterfaceImpl1<T> implements GenericInterface<T> { // 实现类也需要定义泛型参数变量
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}
代码块123456

在主方法中调用实现类的show()方法:

    public static void main(String[] args) {
        GenericInterfaceImpl1<Float> floatGenericInterfaceImpl1 = new GenericInterfaceImpl1<>();
        floatGenericInterfaceImpl1.show(100.1f);
    }
代码块1234

5. 类型通配符

我们先来看一个泛型作为方法参数的实例:

import java.util.ArrayList;
import java.util.List;
public class GenericDemo3 {
    /**
     * 遍历并打印集合中的每一个元素
     * @param list 要接收的集合
     */
    public void printListElement(List<Object> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }
}
代码块1234567891011121314

观察上面的代码,参数list的限定的泛型类型为Object, 也就是说,这个方法只能接收元素为Object类型的集合,如果我们想传递其他元素类型的集合,是行不通的。例如,如果传递装载Integer元素的集合,程序在编译阶段就会报错:

Tips: 泛型中的List<Object>并不是List<Integer>的父类,它们不满足继承关系。

5.1 无限定通配符

想要解决这个问题,使用类型通配符即可,修改方法参数处的代码,将<>中间的Object改为?即可:

public void printListElement(List<?> list) {
代码块1

此处的?就是类型通配符,表示可以匹配任意类型,因此调用方可以传递任意泛型类型的列表。

完整实例如下:

实例演示

import java.util.ArrayList;
import java.util.List;
public class GenericDemo3 {
    /**
     * 遍历并打印集合中的每一个元素
     * @param list 要接收的集合
     */
    public void printListElement(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }
    public static void main(String[] args) {
        // 实例化一个整型的列表
        List<Integer> integers = new ArrayList<>();
        // 添加元素
        integers.add(1);
        integers.add(2);
        integers.add(3);
        GenericDemo3 genericDemo3 = new GenericDemo3();
        // 调用printListElement()方法
        genericDemo3.printListElement(integers);
        // 实例化一个字符串类型的列表
        List<String> strings = new ArrayList<>();
        // 添加元素
        strings.add("Hello");
        strings.add("慕课网");
        // 调用printListElement()方法
        genericDemo3.printListElement(strings);
    }
}
12345678910111213141516171819202122232425262728293031323334

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

1
2
3
Hello
夜猫编程
代码块12345

5.2 extends 通配符

extends通配符用来限定泛型的上限。什么意思呢?依旧以上面的实例为例,我们来看一个新的需求,我们希望方法接收的List 集合限定在数值类型内(float、integer、double、byte 等),不希望其他类型可以传入(比如字符串)。此时,可以改写上面的方法定义,设定上界通配符:

public void printListElement(List<? extends Number> list) {
代码块1

这样的写法的含义为:List集合装载的元素只能是Number自身或其子类(Number类型是所有数值类型的父类),完整实例如下:

实例演示

import java.util.ArrayList;
import java.util.List;
public class GenericDemo4 {
    /**
     * 遍历并打印集合中的每一个元素
     * @param list 要接收的集合
     */
    public void printListElement(List<? extends Number> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }
    public static void main(String[] args) {
        // 实例化一个整型的列表
        List<Integer> integers = new ArrayList<>();
        // 添加元素
        integers.add(1);
        integers.add(2);
        integers.add(3);
        GenericDemo4 genericDemo3 = new GenericDemo4();
        // 调用printListElement()方法
        genericDemo3.printListElement(integers);
    }
}
123456789101112131415161718192021222324252627

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

1
2
3
代码块123

5.3 super 通配符

既然已经了解了如何设定通配符上界,也就不难理解通配符的下界了,可以限定传递的参数只能是某个类型的父类。

语法如下:

<? super Type>
代码块1

Java 反射

本小节我们来学习一个 Java 语言中较为深入的概念 —— 反射(reflection),很多小伙伴即便参与了工作,可能也极少用到 Java 反射机制,但是如果你想要开发一个 web 框架,反射是不可或缺的知识点。本小节我们将了解到 什么是反射反射的使用场景,不得不提的 Class 类,如何通过反射访问类内部的字段、方法以及构造方法等知识点。

1. 什么是反射

Java 的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。反射被视为动态语言的关键。

通常情况下,我们想调用一个类内部的属性或方法,需要先实例化这个类,然后通过对象去调用类内部的属性和方法;通过 Java 的反射机制,我们就可以在程序的运行状态中,动态获取类的信息,注入类内部的属性和方法,完成对象的实例化等操作。

概念可能比较抽象,我们来看一下结合示意图看一下:

图中解释了两个问题:

  1. 程序运行状态中指的是什么时刻Hello.java 源代码文件经过编译得到 Hello.class 字节码文件,想要运行这个程序,就要通过 JVM 的 ClassLoader (类加载器)加载 Hello.class,然后 JVM 来运行 Hello.class,程序的运行期间指的就是此刻;

  2. 什么是反射,它有哪些功能:在程序运行期间,可以动态获得 Hello 类中的属性和方法、动态完成 Hello 类的对象实例化等操作,这个功能就称为反射。

说到这里,大家可能觉得,在编写代码时直接通过 new 的方式就可以实例化一个对象,访问其属性和方法,为什么偏偏要绕个弯子,通过反射机制来进行这些操作呢?下面我们就来看一下反射的使用场景。

2. 反射的使用场景

Java 的反射机制,主要用来编写一些通用性较高的代码或者编写框架的时候使用。

通过反射的概念,我们可以知道,在程序的运行状态中,对于任意一个类,通过反射都可以动态获取其信息以及动态调用对象。

例如,很多框架都可以通过配置文件,来让开发者指定使用不同的类,开发者只需要关心配置,不需要关心代码的具体实现,具体实现都在框架的内部,通过反射就可以动态生成类的对象,调用这个类下面的一些方法。

下面的内容,我们将学习反射的相关 API,在本小节的最后,我将分享一个自己实际开发中的反射案例。

3. 反射常用类概述

学习反射就需要了解反射相关的一些类,下面我们来看一下如下这几个类:

  • ClassClass 类的实例表示正在运行的 Java 应用程序中的类和接口;

  • Constructor:关于类的单个构造方法的信息以及对它的权限访问;

  • Field:Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限;

  • Method:Method 提供关于类或接口上单独某个方法的信息。

字节码文件想要运行都是要被虚拟机加载的,每加载一种类,Java 虚拟机都会为其创建一个 Class 类型的实例,并关联起来。

例如,我们自定义了一个 ImoocStudent.java 类,类中包含有构造方法、成员属性、成员方法等:

public class ImoocStudent {
    // 无参构造方法
    public ImoocStudent() {
    }
    // 有参构造方法
    public ImoocStudent(String nickname) {
        this.nickname = nickname;
    }
    // 昵称
    private String nickname;
    
    // 定义getter和setter方法
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}
代码块1234567891011121314151617181920212223

源码文件 ImoocStudent.java 会被编译器编译成字节码文件 ImoocStudent.class,当 Java 虚拟机加载这个 ImoocStudent.class 的时候,就会创建一个 Class 类型的实例对象:

Class cls = new Class(ImoocStudent);
代码块1

JVM 为我们自动创建了这个类的对象实例,因此就可以获取类内部的构造方法、属性和方法等 ImoocStudent 的构造方法就称为 Constructor,可以创建对象的实例,属性就称为 Field,可以为属性赋值,方法就称为 Method,可以执行方法。

4. Class 类

4.1 Class 类和 class 文件的关系

java.lang.Class 类用于表示一个类的字节码(.class)文件。

4.2 获取 Class 对象的方法

想要使用反射,就要获取某个 class 文件对应的 Class 对象,我们有 3 种方法:

  1. 类名.class:即通过一个 Class 的静态变量 class 获取,实例如下:

Class cls = ImoocStudent.class;
代码块1
  1. 对象.getClass ():前提是有该类的对象实例,该方法由 java.lang.Object 类提供,实例如下:

ImoocStudent imoocStudent = new ImoocStudent("小斧");
Class imoocStudent.getClass();
代码块12
  1. Class.forName (“包名。类名”):如果知道一个类的完整包名,可以通过 Class 类的静态方法 forName() 获得 Class 对象,实例如下:

class cls = Class.forName("java.util.ArrayList");
代码块1

4.3 实例

package com.imooc.reflect;
public class ImoocStudent {
    // 无参构造方法
    public ImoocStudent() {
    }
    // 有参构造方法
    public ImoocStudent(String nickname) {
        this.nickname = nickname;
    }
    // 昵称
    private String nickname;
    // 定义getter和setter方法
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 方法1:类名.class
        Class cls1 = ImoocStudent.class;
        // 方法2:对象.getClass()
        ImoocStudent student = new ImoocStudent();
        Class cls2 = student.getClass();
        // 方法3:Class.forName("包名.类名")
        Class cls3 = Class.forName("com.imooc.reflect.ImoocStudent");
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435363738

代码中,我们在 com.imooc.reflect 包下定义了一个 ImoocStudent 类,并在主方法中,使用了 3 种方法获取 Class 的实例对象,其 forName() 方法会抛出一个 ClassNotFoundException

4.4 调用构造方法

获取了 Class 的实例对象,我们就可以获取 Contructor 对象,调用其构造方法了。

那么如何获得 Constructor 对象?Class 提供了以下几个方法来获取:

  • Constructor getConstructor(Class...):获取某个 public 的构造方法;

  • Constructor getDeclaredConstructor(Class...):获取某个构造方法;

  • Constructor[] getConstructors():获取所有 public 的构造方法;

  • Constructor[] getDeclaredConstructors():获取所有构造方法。

通常我们调用类的构造方法,这样写的(以 StringBuilder 为例):

// 实例化StringBuilder对象
StringBuilder name = new StringBuilder("Hello Imooc");
代码块12

通过反射,要先获取 Constructor 对象,再调用 Class.newInstance() 方法:

实例演示

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectionDemo {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        // 获取构造方法
        Constructor constructor = StringBuffer.class.getConstructor(String.class);
        // 调用构造方法
        Object str = constructor.newInstance("Hello Imooc");
        System.out.println(str);
    }
}
12345678910111213

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

Hello Imooc
代码块1

5. 访问字段

前面我们知道了如何获取 Class 实例,只要获取了 Class 实例,就可以获取它的所有信息。

5.1 获取字段

Field 类代表某个类中的一个成员变量,并提供动态的访问权限。Class 提供了以下几个方法来获取字段:

  • Field getField(name):根据属性名获取某个 public 的字段(包含父类继承);

  • Field getDeclaredField(name):根据属性名获取当前类的某个字段(不包含父类继承);

  • Field[] getFields():获得所有的 public 字段(包含父类继承);

  • Field[] getDeclaredFields():获取当前类的所有字段(不包含父类继承)。

获取字段的实例如下:

package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent1 {
    // 昵称 私有字段
    private String nickname;
    // 余额 私有字段
    private float balance;
    // 职位 公有字段
    public String position;
    public static void main(String[] args) throws NoSuchFieldException {
        // 类名.class 方式获取 Class 实例
        Class cls1 = ImoocStudent1.class;
        // 获取 public 的字段 position
        Field position = cls1.getField("position");
        System.out.println(position);
        // 获取字段 balance
        Field balance = cls1.getDeclaredField("balance");
        System.out.println(balance);
        // 获取所有字段
        Field[] declaredFields = cls1.getDeclaredFields();
        for (Field field: declaredFields) {
            System.out.print("name=" + field.getName());
            System.out.println("\ttype=" + field.getType());
        }
    }
}
代码块1234567891011121314151617181920212223242526272829303132333435

运行结果:

public java.lang.String com.imooc.reflect.ImoocStudent1.position
private float com.imooc.reflect.ImoocStudent1.balance
name=nickname   type=class java.lang.String
name=balance    type=float
name=position   type=class java.lang.String
代码块12345

ImoocStudent1 类中含有 3 个属性,其中 position 为公有属性,nicknamebalance 为私有属性。我们通过类名.class 的方式获取了 Class 实例,通过调用其实例方法并打印其返回结果,验证了获取字段,获取单个字段方法,在没有找到该指定字段的情况下,会抛出一个 NoSuchFieldException

调用获取所有字段方法,返回的是一个 Field 类型的数组。可以调用 Field 类下的 getName() 方法来获取字段名称,getType() 方法来获取字段类型。

5.2 获取字段值

既然我们已经获取到了字段,那么就理所当然地可以获取字段的值。可以通过 Field 类下的 Object get(Object obj) 方法来获取指定字段的值,方法的参数 Object 为对象实例,实例如下:

package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent2 {
    public ImoocStudent2() {
    }
    public ImoocStudent2(String nickname, String position) {
        this.nickname = nickname;
        this.position = position;
    }
    // 昵称 私有字段
    private String nickname;
    // 职位 公有属性
    public String position;
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 实例化一个 ImoocStudent2 对象
        ImoocStudent2 imoocStudent2 = new ImoocStudent2("小斧", "架构师");
        Class cls = imoocStudent2.getClass();
        Field position = cls.getField("position");
        Object o = position.get(imoocStudent2);
        System.out.println(o);
    }
}
代码块123456789101112131415161718192021222324252627282930

运行结果:

架构师
代码块1

ImoocStudent2 内部分别包含一个公有属性 position 和一个私有属性 nickname,我们首先实例化了一个 ImoocStudent2 对象,并且获取了与其对应的 Class 对象,然后调用 getField() 方法获取了 position 字段,通过调用 Field 类下的实例方法 Object get(Object obj) 来获取了 position 字段的值。

这里值得注意的是,如果我们想要获取 nickname 字段的值会稍有不同,因为它是私有属性,我们看到 get() 方法会抛出 IllegalAccessException 异常,如果直接调用 get() 方法获取私有属性,就会抛出此异常。

想要获取私有属性,必须调用 Field.setAccessible(boolean flag) 方法来设置该字段的访问权限为 true,表示可以访问。在 main() 方法中,获取私有属性 nickname 的值的实例如下:

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    // 实例化一个 ImoocStudent2 对象
    ImoocStudent2 imoocStudent2 = new ImoocStudent2("小斧", "架构师");
    Class cls = imoocStudent2.getClass();
    Field nickname = cls.getDeclaredField("nickname");
    // 设置可以访问
    nickname.setAccessible(true);
    Object o = nickname.get(imoocStudent2);
    System.out.println(o);
}
代码块12345678910

此时,就不会抛出异常,运行结果:

小斧
代码块1

5.2 为字段赋值

为字段赋值也很简单,调用 Field.set(Object obj, Object value) 方法即可,第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。我们直接来看实例:

package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent3 {
    public ImoocStudent3() {
    }
    public ImoocStudent3(String nickname) {
        this.nickname = nickname;
    }
    // 昵称 私有字段
    private String nickname;
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 实例化一个 ImoocStudent3 对象
        ImoocStudent3 imoocStudent3 = new ImoocStudent3("小斧");
        Class cls = imoocStudent3.getClass();
        Field nickname = cls.getDeclaredField("nickname");
        nickname.setAccessible(true);
        // 设置字段值
        nickname.set(imoocStudent3, "Colorful");
        // 打印设置后的内容
        System.out.println(imoocStudent3.getNickname());
    }
}
代码块12345678910111213141516171819202122232425262728293031323334353637

运行结果:

Colorful
代码块1

6. 调用方法

Method 类代表某一个类中的一个成员方法。

6.1 获取方法

Class 提供了以下几个方法来获取方法:

  • Method getMethod(name, Class...):获取某个 public 的方法(包含父类继承);

  • Method getgetDeclaredMethod(name, Class...):获取当前类的某个方法(不包含父类);

  • Method[] getMethods():获取所有 public 的方法(包含父类继承);

  • Method[] getDeclareMethods():获取当前类的所有方法(不包含父类继承)。

获取方法和获取字段大同小异,只需调用以上 API 即可,这里不再赘述。

6.2 调用方法

获取方法的目的就是调用方法,调用方法也就是让方法执行。

通常情况下,我们是这样调用对象下的实例方法(以 String 类的 replace() 方法为例):

String name = new String("Colorful");
String result = name.replace("ful", "");
代码块12

改写成通过反射方法调用:

实例演示

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectionDemo1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 实例化字符串对象
        String name = new String("Colorful");
        // 获取 method 对象
        Method method = String.class.getMethod("replace", CharSequence.class, CharSequence.class);
        // 调用 invoke() 执行方法
        String result = (String) method.invoke(name,  "ful", "");
        System.out.println(result);
    }
}
1234567891011121314

运行案例点击 "运行案例" 可查看在线运行效果

运行结果:

Color
代码块1

代码中,调用 Method 实例的 invoke(Object obj, Object...args) 方法,就是通过反射来调用了该方法。

其中 invoke() 方法的第一个参数为对象实例,紧接着的可变参数就是要调用方法的参数,参数要保持一致。

7. 反射应用

Tips: 理解此部分内容可能需要阅读者有一定的开发经验

学习完了反射,大家可能依然非常疑惑,反射似乎离我们的实际开发非常遥远,实际情况也的确是这样的。因为我们在实际开发中基本不会用到反射。下面我来分享一个实际开发中应用反射的案例。

场景是这样的:有一个文件上传系统,文件上传系统有多种不同的方式(上传到服务器本地、上传到七牛云、阿里云 OSS 等),因此就有多个不同的文件上传实现类。系统希望通过配置文件来获取用户的配置,再去实例化对应的实现类。因此,我们一开始的思路可能是这样的(伪代码):

public class UploaderFactory {
    
    // 通过配置文件获取到的配置,可能为 local(上传到本地) qiniuyun(上传到七牛) 
    private String uploader;
    
    // 创建实现类对象的方法
    public Uploader createUploader() {
        switch (uploader) {
            case "local":
                // 实例化上传到本地的实现类
                return new LocalUploader();
            case "qiniuyun":
                // 实例化上传到七牛云的实现类
                return new QiniuUploader();
            default:
                break;
        }
        return null;
    }
}
代码块1234567891011121314151617181920

createUploader() 就是创建实现类的方法,它通过 switch case 结构来判断从配置文件中获取的 uploader 变量。

这看上去似乎没有什么问题,但试想,后续我们的实现类越来越多,就需要一直向下添加 case 语句,并且要约定配置文件中的字符串要和 case 匹配才行。这样的代码既不稳定也不健壮。

换一种思路考虑问题,我们可以通过反射机制来改写这里的代码。首先,约定配置文件的 uploader 配置项不再是字符串,改为类的全路径命名。因此,在 createUploader() 方法中不再需要 switch case 结构来判断,直接通过 Class.forName(uploader) 就可以获取 Class 实例,并调用其构造方法实例化对应的文件上传对象,伪代码如下:

public class UploaderFactory {
    
    // 通过配置文件获取到的配置,实现类的包名.类名
    private String uploader;
    
    // 创建实现类对象的方法
    public Uploader createUploader() {
        // 获取构造方法
        Constructor constructor = Class.forName(uploader).getConstructor();
        return (Uploader) constructor.newInstance();
    }
}
代码块123456789101112

通过反射实例化对应的实现类,我们不需要再维护 UploaderFactory 下的代码,其实现类的命名、放置位置也不受约束,只需要在配置文件中指定类名全路径即可。

1、通过本小节的学习,我们知道了反射是 Java 提供的一种机制,它可以在程序的运行状态中,动态获取类的信息,注入类内部的属性和方法,完成对象的实例化等操作。获取 Class 对象有 3 种方法,通过学习反射的相关接口,我们了解到通过反射可以实现一切我们想要的操作。在本小节的最后,我也分享了一个我在实际开发中应用反射的案例,希望能对大家有所启发。

2、通过本小节的学习,我们知道了使用泛型可以避免强制类型转换,也可以避免运行期就抛出的ClassCastException异常。在使用泛型时,要注意变量声明的泛型类型要匹配传递给实际对象的类型, Java 7 及以后的版本中,构造方法中可以省略泛型类型,推荐直接省略。我们也学习了如何自定义泛型类和泛型方法,在实际的开发中,我们想要编写比较通用的代码就避免不了使用泛型,大家可以在以后的开发中慢慢体悟。另外,泛型也是可以继承的。最后,我们还讲解了类型通配符的概念和使用场景。

3、本小节我们学习了 Java 的集合,它们定义在java.util包中,Java 中的集合主要有CollectionMap两大接口。List集合是元素有序并且可以重复的集合;Set集合是元素无序并且不可以重复的集合;Map是以键值对(key-value)的形式存储的对象之间的映射,它们都支持泛型。我们分别介绍了 3 个接口常用的实现类的用法。同学们要多多进行编码练习。

4、通过本小节的学习,我们知道了枚举类就是一个可以表示枚举的类,当一个类的对象只有有限个、确定个的时候,我们就可以定义一个枚举类来存放这些对象。使用枚举类可以规避编译器无法检测每个值的范围是否合法的问题。自定义枚举类可以有两种方式,更推荐使用enum关键字来定义枚举类。所有通过enum关键字定义的枚举类都继承自java.lang.Enum类,要了解该类的常用方法的使用。

5、通过本小节的学习,我们知道了包装类就是将基本数据类型包装成的类,它有属性、方法,可以对象化交互。除了 int 对应的包装类名称为 Integer 以及 char 对应的包装类名称 Character,其他 6 种数据类型对应的包装类,命名都为其基本数据类型的首字母的大写。装箱就是基本数据类型向包装类转换,拆箱就是包装类向基本数据类型转换。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多