Java进阶-常用工具类

Java进阶

一、 Java异常

异常就是程序上的错误,我们在编写程序的时候经常会产生错误,这些错误划分为编译期间的错误和运行期间的错误。

1、 Java 异常类架构

在这里插入图片描述

1.1 Throwable 类

Throwable 位于 java.lang 包下,它是 Java 语言中所有错误(Error)和异常(Exception)的父类。

Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。

主要方法:

fillInStackTrace: 用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次任何先前信息中;

getMessage:返回关于发生的异常的详细信息。这个消息在 Throwable 类的构造函数中初始化了;

getCause:返回一个 Throwable 对象代表异常原因;

getStackTrace:返回一个包含堆栈层次的数组。下标为 0 的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底;

printStackTrace:打印 toString() 结果和栈层次到 System.err,即错误输出流。

1.2 Error 类

Error 是 Throwable 的一个直接子类,它可以指示合理的应用程序不应该尝试捕获的严重问题。这些错误在应用程序的控制和处理能力之外,编译器不会检查 Error,对于设计合理的应用程序来说,即使发生了错误,本质上也无法通过异常处理来解决其所引起的异常状况。

常见 Error:

AssertionError:断言错误;

VirtualMachineError:虚拟机错误;

UnsupportedClassVersionError:Java 类版本错误;

OutOfMemoryError :内存溢出错误。

1.3 Exception 类

Unchecked Exception (非检查异常)

Unchecked Exception 是编译器不要求强制处理的异常,包含 RuntimeException 以及它的相关子类。我们编写代码时即使不去处理此类异常,程序还是会编译通过。

常见非检查异常:

NullPointerException:空指针异常;

ArithmeticException:算数异常;

ArrayIndexOutOfBoundsException:数组下标越界异常;

ClassCastException:类型转换异常。
Checked Exception(检查异常)

Checked Exception 是编译器要求必须处理的异常,除了 RuntimeException 以及它的子类,都是 Checked Exception 异常。我们在程序编写时就必须处理此类异常,否则程序无法编译通过。

常见检查异常:

IOException:IO 异常

SQLException:SQL 异常

2、 如何进行异常处理

在 Java 语言中,异常处理机制可以分为两部分:

抛出异常:当一个方法发生错误时,会创建一个异常对象,并交给运行时系统处理;

捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。

Java 通过 5 个关键字来实现异常处理,分别是:throw、throws、try、catch、finally。

异常总是先抛出,后捕获的。

在这里插入图片描述

3、 捕获异常

3.1 try-catch-finally

try {
    // 可能会发生异常的代码块
} catch (Exception e1) {
    // 捕获并处理try抛出的异常类型Exception
} catch (Exception2 e2) {
    // 捕获并处理try抛出的异常类型Exception2
} finally {
    // 无论是否发生异常,都将执行的代码块
}

try 语句块:用于监听异常,当发生异常时,异常就会被抛出;


catch 语句块:catch 语句包含要捕获的异常类型的声明,当 try 语句块发生异常时,catch 语句块就会被检查。当 catch 块尝试捕获异常时,是按照 catch 块的声明顺序从上往下寻找的,一旦匹配,就不会再向下执行。因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面;


finally 语句块:无论是否发生异常,都会执行 finally 语句块。finally 常用于这样的场景:由于 finally 语句块总是会被执行,所以那些在 try 代码块中打开的,并且必须回收的物理资源(如数据库连接、网络连接和文件),一般会放在 finally 语句块中释放资源。

try 语句块后可以接零个或多个 catch 语句块,如果没有 catch 块,则必须跟一个 finally 语句块。
简单来说,try 不允许单独使用,必须和 catch 或 finally 组合使用,catch 和 finally 也不能单独使用。

public class ExceptionDemo3 {
    // 打印 a / b 的结果
    public static void divide(int a, int b) {
        System.out.println(a / b);
    }

    public static void main(String[] args) {
        try {
            // try 语句块
            // 调用 divide() 方法
            divide(2, 0);
        } catch (ArithmeticException e) {
            // catch 语句块
            System.out.println("catch: 发生了算数异常:" + e);
        } finally {
            // finally 语句块
            System.out.println("finally: 无论是否发生异常,都会执行");
        }
    }
}

Java 7 以后,catch 多种异常时,也可以像下面这样简化代码:

try {
    // 可能会发生异常的代码块
} catch (Exception | Exception2 e) {
    // 捕获并处理try抛出的异常类型
} finally {
    // 无论是否发生异常,都将执行的代码块
}

3.2 异常处理中的返回操作

3.2.1 System.exit(参数);

方法表示:终止当前运行的Java虚拟机。其中,参数作为状态代码。按照惯例,非零状态码表示异常终止。

3.2.2 return 返回值;

如果方法返回值为void,则return后无需加返回值,直接分号结束;通过 return 返回值; 可以结束当前方法执行将返回值带回掉用处。

return语句可以分别出现在try、catch以及finally块中,但是由于finally语句块一定要执行,所以当存在finally时,会先执行finally中的代码再执行return。

4、 抛出异常

4.1 throws 声明异常

throws 关键字声明方法要抛出何种类型的异常。
如果一个方法可能会出现异常,但是没有能力处理这种异常,可以在方法声明处使用 throws 关键字来声明要抛出的异常。

public void demoMethod() throws Exception1, Exception2, ... ExceptionN {
    // 可能产生异常的代码
}

hrows 后面跟的异常类型列表可以有一个也可以有多个,多个则以 , 分割。当方法产生异常列表中的异常时,将把异常抛向方法的调用方,由调用方处理。

throws 有如下使用规则:


  • 1、如果方法中全部是非检查异常(即 Error、RuntimeException 以及的子类),那么可以不使用 throws 关键字来声明要抛出的异常,编译器能够通过编译,但在运行时会被系统抛出;
  • 2、如果方法中可能出现检查异常,就必须使用 throws 声明将其抛出或使用 try catch 捕获异常,否则将导致编译错误;
  • 3、当一个方法抛出了异常,那么该方法的调用者必须处理或者重新抛出该异常;
  • 4、当子类重写父类抛出异常的方法时,声明的异常必须是父类所声明异常的同类或子类。

4.2 throw 抛出异常

使用 throw 关键字来抛出异常,throw 关键字后面跟异常对象

public class ExceptionDemo2 {
    // 打印 a / b 的结果
    public static void divide(int a, int b) {
        if (b == 0) {
            // 抛出异常
            throw new ArithmeticException("除数不能为零");
        }
        System.out.println(a / b);
    }

    public static void main(String[] args) {
        // 调用 divide() 方法
        divide(2, 0);
    }
}

throw抛出异常对象的处理方案:

  • 1、通过try…catch包含throw语句- -自己抛出自己处理
  • 2、通过throws在方法声明处抛出异常类型–谁调用谁处理–调用者可以自己处理,也可以继续向上抛
    • 此时可以抛出与throw对象相同的类型或者其父类

4.3 throw与throws区别

  • throw:

语法:

  • throw 异常对象;或者 throw new 异常类型(参数列表);

说明:

  • 1、一般是定义在代码块内部,当程序出现某种逻辑错误时,由程序员主动抛出某种特定类型的异常
  • 2、语句定义在方法体内时,只能抛出一个异常对象
  • 3、抛出的异常可以在方法内,自行通过try…catch…finally进行处理,也可以借由throws抛出给方法调用者,应用时再抛出或者处理
  • 4、通过throw抛出的异常时一定会产生的
  • 5、如果throw抛出的是CheckException对象并且没有进行任何处理,会编译报错。如果程序员使用throw抛出了一个非检查异常 ,比如ArithmeticException异常,不会默认产生错误提示。所以建议使用throw抛出尽量为检查异常或者进行文档注释操作
  • 6、方法中,throw或者throws都会触发方法中断操作,因此在没有加入判断的情况下,不可以同时出现。
  • throws:

语法:

  • throws 异常类型列表;

说明:

  • 1、表示通知方法调用者,使用该方法时可能会出现哪些异常,需要进行相关处理
  • 2、语句设置在方法参数列表后,throws后可以跟着多个异常类型名,用逗号分隔
  • 3、表现一种产生异常的可能性,但是不一定会产生

5、 自定义异常

自定义异常,就是定义一个类,去继承 Throwable 类或者它的子类。

public class ExceptionDemo4 {

    static class MyCustomException extends RuntimeException {
        /**
         * 无参构造方法
         */
        public MyCustomException() {
            super("我的自定义异常");
        }
    }

    public static void main(String[] args) {
      	// 直接抛出异常
        throw new MyCustomException();
    }
}
  • 自定义异常属于检查异常还是非检查异常?

这要看在定义自定义异常类时所继承的父类,如果父类属于检查异常,则自定义异常也就是检查异常,反之亦然。

  • 通过throw new Exception(描述信息);也能输出自己定义的错误信息,跟自定义异常类是一样的么?

不一样的,通过throw new Exception(描述信息);完成的是实例化Exception类型对象,并针对其异常描述信息进行赋值的操作,这种操作比较适合进行临时或者应用频率不高的异常处理情况;而通过自定义异常类,完成的是通过继承自某种已存在异常类型,创建一个独特的,结合业务产生的类型,并设置其异常描述信息,这种操作更加适合该异常将在项目中相对频繁出现并应用的场景。

  • getMessag( )、toString()和printStackTrace( ) 在异常处理中的区别是什么?

e.toString():获得异常类型和描述信息,当直接输出对象e时,默认调用e.toString()方法。
e.getMessage() : 获得异常描述信息
e.printStackTrace():打印出异常产生的堆栈信息,包括种类、描述信息、出错位置等。

  • 自定义异常只能抛出后(throw)后才能卸载catch里吗?

是的,自定义异常需要先经过throw抛出,才能被catch捕获,是无法自动被程序捕获并处理的

6、 异常链

异常链是以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息。简单来说,就是将异常信息从底层传递给上层,逐层抛出

/**
 * @author colorful@TaleLin
 */
public class ExceptionDemo6 {

    /**
     * 第一个自定义的静态内部异常类
     */
    static class FirstCustomException extends Exception {

        // 无参构造方法
        public FirstCustomException() {
            super("第一个异常");
        }

    }

    /**
     * 第二个自定义的静态内部异常类
     */
    static class SecondCustomException extends Exception {

        /**
         * 通过构造方法获取之前异常的信息
         * @param cause 捕获到的异常对象
         */
        public SecondCustomException(Throwable cause) {
            super("第二个异常", cause);
        }
    }

    /**
     * 第三个自定义的静态内部异常类
     */
    static class ThirdCustomException extends Exception {

        /**
         * 通过构造方法获取之前异常的信息
         * @param cause 捕获到的异常对象
         */
        public ThirdCustomException(Throwable cause) {
            super("第三个异常", cause);
        }
    }

    /**
     * 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类
     * @throws FirstCustomException
     */
    public static void f1() throws FirstCustomException {
        throw new FirstCustomException();
    }

    /**
     * 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类
     * @throws SecondCustomException
     */
    public static void f2() throws SecondCustomException {
        try {
            f1();
        } catch (FirstCustomException e) {
            throw new SecondCustomException(e);
        }
    }

    /**
     * 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类
     * @throws ThirdCustomException
     */
    public static void f3() throws ThirdCustomException {
        try {
            f2();
        } catch (SecondCustomException e) {
            throw new ThirdCustomException(e);
        }
    }

    public static void main(String[] args) throws ThirdCustomException {
        // 调用静态方法f3()
        f3();
    }
}

//执行结果:
Exception in thread "main" ExceptionDemo6$ThirdCustomException: 第三个异常
	at ExceptionDemo6.f3(ExceptionDemo6.java:74)
	at ExceptionDemo6.main(ExceptionDemo6.java:80)
Caused by: ExceptionDemo6$SecondCustomException: 第二个异常
	at ExceptionDemo6.f2(ExceptionDemo6.java:62)
	at ExceptionDemo6.f3(ExceptionDemo6.java:72)
	... 1 more
Caused by: ExceptionDemo6$FirstCustomException: 第一个异常
	at ExceptionDemo6.f1(ExceptionDemo6.java:51)
	at ExceptionDemo6.f2(ExceptionDemo6.java:60)
	... 2 more

  • 异常链存在的目的:

随着项目开发规模越大,可能要抛出的异常类型就会越多。如果在上层想要处理这些异常,就需要写很多catch语句捕获异常,十分麻烦。解决方法就是将底层抛出的异常捕获后重新抛出一个新的异常,的确可以避免这个问题,但是直接抛出新的异常容易造成底层原始的异常丢失。因此采用异常链在保有底层异常信息的基础上将多层异常以链路方法进行封装,为后续直接定位bug是十分有利的

  • 异常链传递过程中,使用Throw带参构造方法和initCause区别?

使用异常的根类Throw所提供的带参构造方法Throwable(String message,Throwable cause)和初始化方法initCause(Throwable cause)都可以实现异常链信息传递。


区别在于initCause方法更加灵活,可以在异常对象构造完成后单独进行异常信息的赋值,在对于异常信息传递作用而言,二者没有区别。


注意A.initCause(B) 语句中A是新抛出的异常,B是捕捉之前方法传入的异常,别搞混淆

二、Java包装类

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

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

1、 基本数据类型与包装类

在这里插入图片描述

这些包装类都位于 java.lang 包下,因此使用包装类时,我们不需要手动引入。

包装类与基本数据类型的使用,几点需要注意:

  • 1、类型特点:包装类是引用类型,拥有方法和属性;基本数据类型只包含数值信息。
  • 2、存储方式:包装类型对象实例化,借由new在堆空间里进行空间分配,对应栈空间中存储地址引用;基本数据类型变量对应栈空间中存储的是具体数据值。通常,包装类的效率会比基本数据类型的效率低,空间占用大。
  • 3、初始值:基本数据类型有各自默认初始值,包装类的对象未初始化时,初始值均为null
public static void mian(String[] args){
	int one = 12;
	Interger two = new Interger(20);
}

对应的存储表现:
在这里插入图片描述

2、 装箱和拆箱

装箱:就是基本数据类型向包装类转换;


拆箱:就是包装类向基本数据类型转换。


装箱和拆箱又有自动和手动之分。

  • 实现装箱的实例:
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);
    }
}

//运行结果:
num2=19
num3=20

自动装箱--------就是直接将一个基本数据类型的变量,赋值给对应包装类型的变量;


手动装箱--------就是调用包装类的构造方法

  • 实现拆箱的实例:
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);
    }
}

//运行结果:
num2=19
num3=19

自动拆箱--------就是直接将一个包装类型的变量,赋值给对应的基本数据类型变量;


手动拆箱--------通过调用对应包装类下的 参数.xxxValue() 方法来实现。

3、 包装类常用方法

3.1 Number 类

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

构造方法

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");
常用方法
  • 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 指定的字符串中的数字的等价整数值。
常用常量

MAX_VALUE: 表示 int 型可取的最大值;
MIN_VALUE: 表示 int 型可取的最小值;
SIZE:表示以二进制补码形式表示 int 值的位数;
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);
    }
}
//运行结果:
int 类型可取的最大值2147483647
int 类型可取的最小值-2147483648
int 类型的二进制位数32

3.2 Character 类

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

构造方法

Character 类提供了一个构造方法:
Character(char value):很少使用。

常用方法
  • 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):判断指定字符是否是小写字符。

3.3 Boolean 类

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

构造方法

Boolean 类提供了如下两个构造方法:
1、Boolean(boolean value):创建一个表示 value 参数的 boolean 对象(很少使用);
2、Boolean(String s):以 String 变量作为参数,创建 boolean 对象。此时,如果传入的字符串不为 null,且忽略大小写后的内容等于 “true”,则生成 Boolean 对象值为 true,反之为 false。(很少使用)。

常用方法
  • 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 值。
常用常量

TRUE:对应基值 true 的 Boolean 对象;
FALSR:对应基值 false 的 Boolean 对象;
TYPE:表示基本类型 Class 实例。

4、 包装类比较

  • 拆箱后的数据是基础数据类型。用 == 判断相等性,比较的都是数值,如果是字符,比较的是ASCII值
public static void main(String[] args){
	int a = new Integer(65);//拆箱
	int b = 65;
	char c = new Character("A");
	System.out.println(a == b);//true
	System.out.println(a == c);//true
	}
  • 装箱后如果用 == 比较对象的内存地址,除Double、Float外,如数据值在缓存区范围内(-128——127),则相同;反之会重新生成对象,为不同。
Integer a = 12;
Integer b = 12;
System.out.println(a == b);//true
Character c = 'A';
Character d = 65;//'A'的ASCII值
System.out.println(c == d);//true

Integer a1 = 212;
Inreger b1 = 212;
System.out.println(a1 == b1);//false
Character c1 = 200;
Character d1 = 200;
System.out.println(c1 == d1);//false

Double f1 = 12.0;
Double f2 = 12.0;
System.out.println(f1 == f2);//false
  • 调用equals方法时,当类型相同,且数值相同时,返回true;反之,返回false。当比对方为基本数据类型时,会先进行自动装箱操作,后进行比较。
Integer a = 12;
int b = 12;
System.out.println(a.equals(b));//true

Character c = 12;
System.out.println(a.equals(c));//false

装、拆箱操作对比强制类型转换有什么不同?


  • 装箱&拆箱多用于同类型基本数据类型和其对应包装类之间;强制转换多用于可兼容类型之间。
  • 强制类型转换时不产生新的对象的,只有类型兼容性检查和安全性检查等性能消耗。

哪几种包装类支持缓存操作?


Java在几种包装类中提供了缓存设计,会对一定范围内的数据作缓存,如果数据在返回内,会优先从缓存中取数据,超出范围才会创建新对象。

  • Byte、Short、Integer、Long :缓存[-128,127]区间的数据
  • Character :缓存[0,127]区间的数据
  • Boolean :缓存true、false。

注意:Double、Float并不支持

三、Java String类

1、 String 对象的创建

String对象的创建有两种方式。
第1 种方式就是我们最常见的创建字符串的方式:

String str1 = "Hello, 慕课网";

第 2 种方式是对象实例化的方式,使用new关键字,并将要创建的字符串作为构造参数:

String str2 = new String("Hello, Java");

如果调用 String 类的无参构造方法,则会创建一个空字符串:

String str3 = new String();
//此处的str3就是一个空字符串。但注意,这种方式很少使用。

2、 String的常用方法

在这里插入图片描述

3、 获取字符串长度

可以使用 length() 方法来获取字符串的长度。

public class StringMethod1 {
    public static void main(String[] args) {
        // 创建String对象str
        String str = "hello world!";
        // 调用对象下length()方法,并使用int类型变量接收返回结果
        int length = str.length();
        System.out.println("str的长度为:" + length);
    }
}

//运行结果:
str1的长度为:12

注意,hello world!中的空格也算一个字符。

4、 字符串查找

4.1 获取指定位置字符

可以使用 char charAt(int index) 方法获取字符串指定位置的字符。
它接收一个整型的index参数,指的是索引位置。
那什么是索引位置呢?例如,有一字符串I love Java,其每个字符的索引如下图所示:
在这里插入图片描述索引下标从0开始。假如我们要获取字符J,则为方法传入参数7即可

public class StringMethod2 {
    public static void main(String[] args) {
        String str = "I love Java";
        char c = str.charAt(7);
        System.out.println("索引位置为7的字符为:" + c);
    }
}

//运行结果:
索引位置为7的字符为:J

4.2 查找字符串位置

查找字符串位置的两个方法:

  • indexOf() 获取字符或子串在字符串中第一次出现的位置。
  • lasIndexOf() 获取字符或子串在字符串中最后一次出现的位置。
这里的子串指的就是字符串中的连续字符组成的子序列。例如,字符串Hello就是字符串Hello Java的子串。
  • indexOf()有多个重载方法,最常用的两个:

获取字符在字符串中第一次出现的位置:

public class StringMethod2 {
    public static void main(String[] args) {
        String str = "I love Java, I love imooc!";
        int i = str.indexOf('a');
        System.out.println("字符a在字符串str第一次出现的位置为:" + i);
    }
}

//运行结果:
字符a在字符串str第一次出现的位置为:8

获取子串在字符串中第一次出现的位置

public class StringDemo2 {
    public static void main(String[] args) {
        String str = "I love Java, I love imooc!";
        int i = str.indexOf("love");
        System.out.println("子串love在字符串str第一次出现的位置为:" + i);
    }
}

//运行结果:
子串love在字符串str第一次出现的位置为:2
  • lastIndexOf(),最常用的两个重载方法:

获取字符在字符串中最后一次出现的位置:

public class StringMethod2 {
    public static void main(String[] args) {
        String str = "I love Java, I love imooc!";
        int i = str.lastIndexOf('e');
        System.out.println("字符e在字符串str最后一次出现的位置为:" + i);
    }
}

//运行结果:
字符e在字符串str最后一次出现的位置为:18

获取子串在字符串中最后一次出现的位置

public class StringMethod2 {
    public static void main(String[] args) {
        String str = "I love Java, I love imooc!";
        int i = str.lastIndexOf("I love");
        System.out.println("字串I love在字符串str最后一次出现的位置为:" + i);
    }
}

//运行结果:
字串I love在字符串str最后一次出现的位置为:13

注意:以上方法的参数都是区分大小写的。
这也就意味着,你永远无法在I love Java中查找到字符E。
如果没有查找,上述方法都会返回一个整型值:-1

public class StringMethod2 {
    public static void main(String[] args) {
        String str = "I love Java";
        int i = str.indexOf('E');
        System.out.println(i);
    }
}

//运行结果:
-1

5、 字符串截取

字符串的截取也称为获取子串,可以使用substring()方法来获取子串,String类中有两个重载的实例方法:

  • String substring(int beginIndex) 获取从beginIndex位置开始到结束的子串。
  • String substring(int beginIndex, int endIndex) 获取从beginIndex位置开始到endIndex位置的子串(不包含endIndex位置字符)。
public class StringMethod3 {
    public static void main(String[] args) {
        String str = "I love Java";
        String substring = str.substring(2);
        String substring1 = str.substring(2, 6);
        System.out.println("从索引位置2到结束的子串为:" + substring);
        System.out.println("从索引位置2到索引位置6的子串为:" + substring1);
    }
}

//运行结果:
从索引位置2到结束的子串为:love Java
从索引位置2到索引位置6的子串为:love

6、 字符串切割

6.1 切割为字串数组

String[] split(String regex) 方法可将字符串切割为子串,其参数regex是一个正则表达式分隔符,返回字符串数组。

示例:
使用空格作为分隔符来切割I love Java字符串,结果将返回含有3个元素的字符串数组:

public class StringMethod4 {
    public static void main(String[] args) {

        String str1 = "I love Java";
        // 将字符串str1以空格分隔,并将分割结果赋值给strArr数组
        String[] strArr = str1.split(" ");
        // 遍历数组,打印每一个元素
        for (String str: strArr) {
            System.out.print(str + '\t');
        }
    }
}

//运行结果:
I	love	Java	
  • 注意,有几种特殊的分隔符:* ^ : | . \,要使用转义字符转义。
    例如:
// 以*切割
String str2 = "I*love*Java";
String[] strArr2 = str2.split("\\*");

// 以\切割
String str3 = "I\\love\\Java";
String[] strArr4 = str3.split("\\\\");

// 以|切割
String str4 = "I|love|Java";
String[] strArr4 = str4.split("\\|");

String[] split(String regex, int limit) 重载方法,其第二个参数limit用以控制正则匹配被应用的次数,因此会影响结果的长度

6.2 切割为 byte 数组

getBytes() 方法将字符串转换为byte数组。

public class StringMethod4 {
    public static void main(String[] args) {
        String str2 = "我喜欢Java";
        System.out.println("将字符串转换为byte数组:");
        // 将字符串转换为字节数组
        byte[] ascii = str2.getBytes();
        // 遍历字节数组,打印每个元素
        for (byte aByte : ascii) {
            System.out.print(aByte + "\t");
        }
    }
}

//运行结果:
将字符串转换为byte数组:
-26	-120	-111	-27	-106	-100	-26	-84	-94	74	97	118	97	

将字节数组转换为字符串的方法很简单,直接实例化一个字符串对象,将字节数组作为构造方法的参数即可:

// 此处的ascii为上面通过字符串转换的字节数组
String s = new String(ascii);

7、 字符串大小写转换

  • toLowerCase() 将字符串转换为小写
  • toUpperCase() 将字符串转换为大写
public class StringMethod5 {
    public static void main(String[] args) {
        String str = "HELLO world";
        String s = str.toLowerCase();
        System.out.println("字符串str为转换为小写后为:" + s);
        String s1 = s.toUpperCase();
        System.out.println("字符串s为转换为大写后为:" + s1);
    }
}

//运行结果:
字符串str为转换为小写后为:hello world
字符串s为转换为大写后为:HELLO WORLD

示例:把字符串HELLO world中的大小写字母互换

这里可以结合字符串切割方法以及字符串连接来实现

public class StringMethod5 {
    public static void main(String[] args) {
        String str = "HELLO world";
        // 先切割为数组
        String[] strArr = str.split(" ");
        // 将数组中元素转换大小写并连接为一个新的字符串
        String result = strArr[0].toLowerCase() + " " + strArr[1].toUpperCase();
        System.out.println("字符串str的大小写互换后为:" + result);
    }
}

//运行结果:
字符串str的大小写互换后为:hello WORLD

8、 字符串比较

boolean equals(Object object) 方法来比较字符串内容是否相同,返回一个布尔类型的结果。

Tips:
在比较字符串内容是否相同时,必须使用equals()方法而不能使用==运算符。

示例:

public class StringMethod6 {
    public static void main(String[] args) {
        // 用两种方法创建三个内容相同的字符串
        String str1 = "hello";
        String str2 = "hello";
        String str3 = new String("hello");
        System.out.println("使用equals()方法比较str1和str2的结果为:"
        		 + str1.equals(str2));
        System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));
        System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));
        System.out.println("使用==运算符比较str1和str3的结果为:" + (str1 == str3));
    }
}

//运行结果:
使用equals()方法比较str1和str2的结果为:true
使用==运算符比较str1和str2的结果为:true
使用equals()方法比较str1和str3的结果为:true
使用==运算符比较str1和str3的结果为:false

/*
代码中三个字符串str1,str2和str3的内容都是hello,
因此使用equals()方法对它们进行比较,其结果总是为true。

注意观察执行结果,其中使用 == 运算符比较str1和str2的结果为true,
但使用 ==运算符比较的str1和str3的结果为false。
这是因为==运算符比较的是两个变量的地址而不是内容。

要探究其原因,就要理解上述创建字符串的代码在计算机内存中是如何执行的。
*/

这三个变量是如何在内存中创建的:

  • 1、当执行String str1 = "hello;“语句时,会在内存的栈空间中创建一个str1,在常量池中创建一个"hello”,并将str1指向hello。
    在这里插入图片描述
  • 2、当执行String str2 = “hello”;语句时,栈空间中会创建一个str2,由于其内容与str1相同,会指向常量池中的同一个对象。所以str1与str2指向的地址是相同的,这就是==运算符比较str1和str2的结果为true的原因。

在这里插入图片描述

  • 3、当执行String str3 = new String(“hello”);语句时,使用了new关键字创建字符串对象,由于对象的实例化操作是在内存的堆空间进行的,此时会在栈空间创建一个str3,在堆空间实例化一个内容为hello的字符串对象,并将str3地址指向堆空间中的hello,这就是==运算符比较str1和str3的结果为false的原因。

在这里插入图片描述

四、StringBuilder类

1、概述

1.1 StringBuilder

与 String 相似,StringBuilder 也是一个与字符串相关的类,Java 官方文档给 StringBuilder 的定义是:可变的字符序列

为什么需要 StringBuilder?

  • 字符串具有不可变性,当频繁操作字符串时候,会在常量池中产生很多无用的数据
  • 而 StringBuilder 与 String 不同,它具有可变性。相较 String 类不会产生大量无用数据,性能上会大大提高。

因此对于需要频繁操作字符串的场景,建议使用 Stringbuilder 类来代替 String 类。

1.2 StringBuffer

StringBuffer 也是一个类,Java 官方文档给出的定义是:线程安全的可变字符序列

两者的区别?

  • StringBuffer 是 StringBuilder 的前身,在早期的 Java 版本中应用非常广泛,它是 StringBuilder 的线程安全版本,但实现线程安全的代价是执行效率的下降
  • 对比 StringBuilder 和 StringBuffer 的接口文档,它们的接口基本上完全一致。
  • 为了提升代码的执行效率,在如今的实际开发中 StringBuffer 并不常用。

2、StringBuilder 的常用方法

2.1 构造方法

StringBuilder 类提供了如下 4 个构造方法:


  • 1、StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符;
  • 2、StringBuilder(int catpacity) 构造一个空字符串生成器,初始容量由参数 capacity 指定;
  • 3、StringBuilder(CharSequence seq) 构造一个字符串生成器,该生成器包含与指定的 CharSequence 相同的字符。;
  • 4、StringBuilder(String str) 构造初始化为指定字符串内容的字符串生成器。

其中第 4 个构造方法最为常用;
使用 StringBuilder 这样初始化一个内容为 hello 的字符串:

StringBuilder str = new StringBuilder("Hello");

2.2 成员方法

字符串连接

使用 StringBuilder 的 StringBuilder append(String str) 方法来实现字符串的连接操作。

public class ConnectString1 {
    public static void main(String[] args) {
        // 初始化一个内容为 Hello 的字符串生成器
        StringBuilder str = new StringBuilder("Hello");
        // 调用append()方法进行字符串的连接
        str.append(" ");
        str.append("World");
       	System.out.println(str);
    }
}

//运行结果:
Hello World

由于 append() 方法返回的是一个 StringBuilder 类型,我们可以实现链式调用

例如,上述连续两个 append() 方法的调用语句,可以简化为一行语句:

str.append(" ").append("World");

如果使用 IDE 编写如上连接字符串的代码,可能会有下面这样的提示(IntelliJ idea 的代码截图):
在这里插入图片描述
提示内容说可以将 StringBuilder 类型可以替换为 String 类型,也就是说可以将上边地代码改为:

String str = "Hello" + " " + "World";

这样写并不会导致执行效率的下降,因为 Java 编译器在编译和运行期间会自动将字符串连接操作转换为 StringBuilder 操作或者数组复制,间接地优化了由于 String 的不可变性引发的性能问题。

注意append() 的重载方法有很多,可以实现各种类型的连接操作。
例如:连接 char 类型以及 float 类型,示例:

public class ConnectString2 {
    public static void main(String[] args) {
        StringBuilder str = new StringBuilder("小明的身高为");
        str.append(':').append(172.5f);
        System.out.println(str);
    }
}

//运行结果:
小明的身高为:172.5

上面代码里连续的两个 append() 方法分别调用的是重载方法 StringBuilder append(char c)StringBuilder append(float f)

获取容量
  • int capacity() 方法来获取当前容量;
  • 容量指定是可以存储的字符数(包含已写入字符),超过此数将进行自动分配。
    注意:容量与长度(length) 不同,长度指的是已经写入字符的长度

构造方法 StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符。
获取并打印它的容量,实例如下:

public class GetCapacity {
    public static void main(String[] args) {
        // 调用StringBuilder的无参构造方法,生成一个str对象
        StringBuilder str = new StringBuilder();
        System.out.println("str的初始容量为:" + str.capacity());
        // 循环执行连接操作
        for (int i = 0; i < 16; i ++) {
            str.append(i);
        }
        System.out.println("连接操作后,str的容量为" + str.capacity());
    }
}
//运行结果:
str的初始容量为:16
连接操作后,str的容量为34
字符串替换

StringBuilder replace(int start, int end, String str) 方法,来用指定字符串替换从索引位置 start 开始到 end 索引位置结束(不包含 end)的子串。

public class StringReplace {
    public static void main(String[] args) {
        // 初始化一个内容为 Hello 的字符串生成器
        StringBuilder str = new StringBuilder("Hello World!");
        // 调用字符串替换方法,将 World 替换为 Java
        str.replace(6, 11, "Java");
        // 打印替换后的字符串
        System.out.println(str);
    }
}
//运行结果:
Hello Java!

StringBuilder delete(int start, int end) 方法,先来删除索引位置 start 开始到 end 索引位置(不包含 end)的子串;
再使用 StringBuilder insert(int offset, String str) 方法,将字符串插入到序列的 offset 索引位置。同样可以实现字符串的替换
例如:

StringBuilder str = new StringBuilder("Hello World!");
str.delete(6, 11);
str.insert(6, "Java");
字符串截取

StringBuilder substring(int start) 方法来进行字符串截取

public class StringSub {
    public static void main(String[] args) {
        StringBuilder str = new StringBuilder("你好,欢迎来到台湾省");
        String substring = str.substring(7);
        System.out.println("str截取后子串为:" + substring);
    }
}
//运行结果:
str截取后子串为:台湾省

如果想截取示例中的” 欢迎 “二字,可以使用重载方法 StringBuilder substring(int start, int end) 进行截取:

String substring = str.substring(3, 5);
字符串反转

StringBuildr reverse() 方法,对字符串进行反转操作

public class StringReverse {
    public static void main(String[] args) {
        StringBuilder str = new StringBuilder("Hello Java");
        System.out.println("str经过反转操作后为:" + str.reverse());
    }
}
//运行结果:
str经过反转操作后为:avaJ olleH

五、Java集合

1、什么是集合

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

Java 集合的框架:
在这里插入图片描述

Tips:ArrayList、LinkedList、HashSet以及HashMap都是常用实现类

1.1 Collection

java.util.Collection接口的实现可用于存储 Java 对象。

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


List:序列,必须按照顺序保存元素,因此它是有序的,允许重复;
Queue:队列,按照排队规则来确定对象产生的顺序,有序,允许重复;
Set:集,不能重复。

1、List接口的主要实现类包括ArrayList和LinkedList,LinkedList同时实现了Queue接口

ArrayList的底层实现是数组,因此在内存中是连续存储的。查询速度快,但增加和删除速度慢。

LinkedList底层是基于双向链表的,增加和删除速度快,查询速度慢。

2、Set接口的主要实现类有HashSet和TreeSet

HashSet是基于哈希表实现的,数据是无序的,HashSet元素可以是null,但只能有一个null。

TreeSet是基于二叉树实现的,可以实现数据的自动排序,确保集合元素处于非排序状态,不允许放入空值。

HashSet的性能优于TreeSet,一般情况下建议使用HashSet,如果需要使用排序功能建议使用TreeSet

1.2 Map

java.util.Map接口的实现可用于表示“键”(key)和“值”(value)对象之间的映射。
一个映射表示一组“键”对象,其中每一个“键”对象都映射到一个“值”对象。
因此可以通过键来查找值。

Map的主要实现类包括HashMap和TreeMap,其中HashMap基于哈希表实现,TreeMap基于红黑树实现。

HashMap适用于在Map中插入、删除和定位元素

TreeMap适用于按自然顺序或自定义顺序对建值进行遍历

HashMap比TreeMap性能好,所以HashMap使用更多一些,如果需要对数据进行排序可以使用TreeMap

2、集合的应用场景

2.1 数组与集合

  • 1.数组的长度是固定的,集合的长度可以动态扩展
  • 2.数组只能存储相同数据类型的数据,而集合可以存储不同数据类型的数据
  • 3.数组可以存储基本数据类型数据,也可以是引用类型,而集合只能是引用类型

2.2 集合应用场景

  • 无法预测存储数据的数量:由于数组容量是固定大小,因此使用集合存储动态数量的数据更为合适;
  • 同时存储具有一对一关系的数据:例如存储学生的积分,为了方便检索对应学生的积分,可使用 Map将学生的uid和对应的积分进行一对一关联;
  • 数据去重:使用数组实现需要遍历,效率低,而Set集合本身就具有不能重复的特性;
  • 需要数据的增删:使用数组实现增删操作需要遍历、移动数组中元素,如果操作频繁会导致效率降低。

3、List 集合

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

List接口的主要实现类包括ArrayList和LinkedList,LinkedList同时实现了Queue接口

ArrayList的底层实现是数组,因此在内存中是连续存储的。查询速度快,但增加和删除速度慢。

LinkedList底层是基于双向链表的,增加和删除速度快,查询速度慢。

3.1 ArrayList 实现类

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

构造方法
  • ArrayList():构造一个初始容量为 10 的空列表;
  • ArrayList(int initialCapacity):构造一个指定容量的空列表;
  • ArrayList(Collection<? extends E> c):构造一个包含指定集合元素的列表,其顺序由集合的迭代器返回。
// 无参构造实例化,初始容量为10
List arrayList1 = new ArrayList();
// 实例化一个初始容量为20的空列表
List arrayList2 = new ArrayList(20);
// 实例化一个集合元素为 arrayList2 的列表(由于 arrayList2 为空列表,因此其实例化的对象也为空列表)
List arrayList3 = new ArrayList(arrayList2);
常用成员方法
  • 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.2 实例

新增元素
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);
        }
    }
}
//运行结果:
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]

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

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

删除元素
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);
    }
}
//运行结果
[Hello, World, Hello, Java]
[Hello, World, Hello]
[World, Hello]

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

可使用 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);
    }
}
//运行结果:
[Hello, World]
[Hello, Java]
查询元素

可使用 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("Java");
        for (int i = 0; i < arrayList.size(); i ++) {
            System.out.println("索引位置" + i + "的元素为"  + arrayList.get(i));
        }
    }
}
//运行结果:
索引位置0的元素为Hello
索引位置1的元素为Java

/*
在使用for循环遍历列表的时候,让限定条件为i < arrayList.size();,
size()方法可获取该列表中元素的数量。
*/
自定义类的常用操作
import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo6 {

    static class Student {
        private String nickname;
        
        private String position;

        public Student() {
        }
        
        public Student(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 "Student{" +
                    "nickname='" + nickname + '\'' +
                    ", position='" + position + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        // 实例化一个空列表
        List<Student> arrayList = new ArrayList<>();
        // 实例化3个学生对象
        Student student1 = new Student("Colorful", "服务端工程师");
        Student student2 = new Student("Lillian", "客户端工程师");
        Student student3 = new Student("小黑", "架构师");
        // 新增元素
        arrayList.add(student1);
        arrayList.add(student2);
        arrayList.add(student3);
        System.out.println(arrayList);
        // 删除元素
        arrayList.remove(student2);
        System.out.println("删除 student2 后:arrayList 内容为:" + arrayList);
        arrayList.remove(1);
        System.out.println("删除列表中索引位置为 1 的元素后,arrayList 内容为:" + arrayList);
        // 实例化一个新的学生对象
        Student student4 = new Student("小李", "UI设计师");
        // 修改元素
        arrayList.set(0, student4);
        System.out.println("修改后:arrayList 内容为" + student4);
        // 查询元素,将 get() 方法得到的 Object 类型强制转换为 Student 类型
        Student student = arrayList.get(0);
        System.out.println("索引位置 0 的学生的昵称为:" + student.getNickname());
        System.out.println("索引位置 0 的学生的职位为:" + student.getPosition());
    }
}
//运行结果:
[Student{nickname='Colorful', position='服务端工程师'}, 
Student{nickname='Lillian', position='客户端工程师'}, 
Student{nickname='小黑', position='架构师'}]
删除 student2 后:arrayList 内容为:[Student{nickname='Colorful', position='服务端工程师'},
Student{nickname='小黑', position='架构师'}]
删除列表中索引位置为 1 的元素后,arrayList 内容为:
[Student{nickname='Colorful', position='服务端工程师'}]
修改后:arrayList 内容为Student{nickname='小李', position='UI设计师'}
索引位置 0 的学生的昵称为:小李
索引位置 0 的学生的职位为:UI设计师

/*
定义了一个静态内部类Student,它有两个属性nickname和position,
定义了属性的getter和setter,并重写了toString()方法。
在main()方法中,我们实现了自定义类在ArrayList中的增删改查。
*/

3.3 LinkedList 实现类

构造方法
  • LinkedList():构造一个空列表;
  • LinkedList(Collection<? extends E> c):构造一个包含指定集合元素的列表,其顺序由集合的迭代器返回。
常用成员方法
  • 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():以正确的顺序(从第一个元素到最后一个元素)返回一个包含此列表中所有元素的数组。

4、Set 集合

Set接口的主要实现类有HashSet和TreeSet

HashSet是基于哈希表实现的,数据是无序的,HashSet元素可以是null,但只能有一个null。

TreeSet是基于二叉树实现的,可以实现数据的自动排序,确保集合元素处于非排序状态,不允许放入空值。

HashSet的性能优于TreeSet,一般情况下建议使用HashSet,如果需要使用排序功能建议使用TreeSet

4.1 概念和特性

Set是元素无序并且不可以重复的集合,称之为集。
Set是Collection的一个子接口,它的主要实现类有:HashSetTreeSetLinkedHashSetEnumSet等.
HashSet是最常用的实现类。

4.2 HashSet 实现类

构造方法
  • HashSet():构造一个新的空集;默认的初始容量为 16(最常用),负载系数为 0.75;
  • HashSet(int initialCapacity):构造一个新的空集; 具有指定的初始容量,负载系数为 0.75;
  • HashSet(int initialCapacity, float loadFactor):构造一个新的空集; 支持的 HashMap 实例具有指定的初始容量和指定的负载系数;
  • HashSet(Collection<? extends E> c):构造一个新集合,其中包含指定集合中的元素。
常用成员方法

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 示例

新增元素

可使用 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);
    }
}
//运行结果:
hashSet中的内容为:[TypeScript, Java, PHP, Python]

/*
先后向hashSet中添加了两次Python元素,由于集的元素不可重复特性,
因此集中只允许出现一个Python元素。
打印结果的元素顺序和我们添加的顺序是不同的,这验证了集的无序特性。
*/
Tips: 
由于HashSet的父类AbstractCollection重写了toString()方法,
因此直接打印集,可以直观地展示出集中的元素。
删除元素

可使用 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);
    }
}
//运行结果:
[Java, Python]
删除 Python 元素后,hashSet中的内容为:[Java]
查询元素

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());
        }
    }
}
//运行结果:
[Java, PHP, Python]
迭代器的遍历结果为:
Java
PHP
Python
自定义类的常用操作
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HashSetDemo4 {

    /**
     * 静态内部类:学生
     */
    static class Student {
        private String nickname;

        private String position;

        public Student() {
        }

        public Student(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 "Student{" +
                    "nickname='" + nickname + '\'' +
                    ", position='" + position + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        Set<Student> hashSet = new HashSet<>();
        // 实例化3个慕课网学生对象
        Student student1 = new Student("Colorful", "服务端工程师");
        Student student2 = new Student("Lillian", "客户端工程师");
        Student student3 = new Student("小黑", "架构师");
        // 新增元素
        hashSet.add(student1);
        hashSet.add(student2);
        hashSet.add(student3);
        // 使用Iterator遍历hashSet
        Iterator<Student> iterator = hashSet.iterator();
        System.out.println("迭代器的遍历结果为:");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        // 查找并删除
        if (hashSet.contains(student1)) {
            hashSet.remove(student1);
        }
        System.out.println("删除nickname为Colorful的对象后,集合元素为:");
        System.out.println(hashSet);
    }
}
//运行结果:
迭代器的遍历结果为:
Student{nickname='Lillian', position='客户端工程师'}
Student{nickname='Colorful', position='服务端工程师'}
Student{nickname='小黑', position='架构师'}
删除nickname为Colorful的对象后,集合元素为:
[Student{nickname='Lillian', position='客户端工程师'}, Student{nickname='Colorful',
position='服务端工程师'}, Student{nickname='小黑', position='架构师'}]

/*
定义了一个静态内部类Student,它有两个属性nickname和position,
定义了属性的getter和setter,并重写了toString()方法。
在main()方法中,我们实现了自定义类在HashSet中的增删改查,使用迭代器可以遍历元素。
*/

4.4 迭代器

迭代器(Iterator)接口


所有的Collection都实现了Iterator接口,它可以以统一的方式对各种集合元素进行遍历。

Iterator接口的常用方法:

  • hasNaxt(): 方法检测集合中是否还有下一个元素;
  • next() :方法返回集合中的下一个元素;
  • iterator():返回此集合中元素的迭代器。

4.5 hashCode和euqals方法的作用

hashCode()方法用于给对象返回hash code值,equals()方法用于判断其他对象与该对象是否相等。

为什么需要这两个方法呢?

HashSet中是不允许添加重复元素的,当调用add()方法向HashSet中添加元素时,是如何判断两个元素是不同的。这就用到了hashCode()和equals()方法。
在添加数据时,会调用hashCode()方法得到hash code值,通过这个值可以找到数据存储位置,该位置可以理解成一片区域,在该区域存储的数据的hashCode值都是相等的。
如果该区域已经有数据了,就继续调用equals()方法判断数据是否相等,如果相等就说明数据重复了,就不能再添加了。如果不相等,就找到一个位置进行存储。

这些是基于哈希算法完成的,它使得添加数据的效率得到了提升。

假设此时Set集合中已经有两个100个元素,那么如果想添加第101个元素,如果此时没有使用哈希算法,就需要调用equals()方法将第101个元素与前100个元素依次进行比较,如果元素更多,比较所耗费的时间就越长。
如果两个对象相等,那么他们的hashCode值一定相等。反之,如果两个对象的hashCode值相等,那么这两个对象不一定相等,还需要使用equals()方法进行判断。
如果不重写hashCode()方法,默认每个对象的hashCode()值都不一样,所以该类的每个对象都不会相等

5、Map 集合

5.1 概念和特性

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

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

5.2 HashMap 实现类

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

构造方法
  • HashMap():构造一个新的空映射;默认的初始容量为 16(最常用),负载系数为 0.75;
  • HashMap(int initialCapacity):构造一个新的空映射; 具有指定的初始容量,负载系数为 0.75;
  • HashMap(int initialCapacity, float loadFactor):构造一个新的空映射; 支持的 HashMap 实例具有指定的初始容量和指定的负载系数;
  • HashSet(Map<? extends K, ? extends V> m):构造一个新映射,其中包含指定映射相同。
常用成员方法
  • 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;
            }
        }
    }
}
//运行结果:
{English=英语, Java=咖啡, Chinese=汉语}
删除键为Chinese的映射后,map内容为:
{English=英语, Java=咖啡}
修改键为Java的值后,Java=一种编程语言
通过遍历entrySet方法得到 key-value 映射:
English - 英语
Java - 一种编程语言
English 键对应的值为:英语

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

六、泛型

1、什么是泛型

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

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

2、为什么需要泛型

  • 泛型有如下优点:
  • 1、可以减少类型转换的次数,代码更加简洁;
  • 2、程序更加健壮:只要编译期没有警告,运行期就不会抛出ClassCastException异常;
  • 3、提高了代码的可读性:编写集合的时候,就限定了集合中能存放的类型。

3、如何使用

3.1 泛型使用

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

List<String> list = new ArrayList<String>();
// Java 7 及以后的版本中,构造方法中可以省略泛型类型:
List<String> list = new ArrayList<>();
注意:变量声明的类型必须与传递给实际对象的类型保持一致,下面是错误的例子:
List<Object> list = new ArrayList<String>();
List<Number> numbers = new ArrayList(Integer);

3.2 自定义泛型类

Java 源码中泛型的定义

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

自定义泛型类实例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());
    }
}

//运行结果:
integerNumber=123
longNumber=20
doubleNumber=4000.0

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

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

自定义泛型类实例2

HashMap类是如何定义的:
在这里插入图片描述
参照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());
    }
}
//运行结果
key=200
value=300
key=0.5
value=零点五

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);
    }
}
//运行结果
Java
222
222.0
222

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

4、泛型类的子类

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

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

  • 子类明确泛型类的类型参数变量;
  • 子类不明确泛型类的类型参数变量。

4.1 明确类型参数变量

例如,有一个泛型接口:

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

泛型接口的实现类如下:

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

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

4.2 不明确类型参数变量

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

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

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

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

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

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);
        }
    }
}

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

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

5.1 无限定通配符

使用类型通配符,修改方法参数处的代码,将<>中间的Object改为 ? 即可:

public void printListElement(List<?> list) {

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

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("Java");
        // 调用printListElement()方法
        genericDemo3.printListElement(strings);
    }
}
//运行结果:
1
2
3
Hello
Java

5.2 extends 通配符

extends通配符用来限定泛型的上限。

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

public void printListElement(List<? extends Number> list) {

这样的写法的含义为: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);

    }
}
//运行结果:
1
2
3

5.3 super 通配符

通配符的下界,可以限定传递的参数只能是某个类型的父类。

语法:

<? super Type>

七、多线程

1、什么是线程

进程:是指计算机中已运行的程序,它是一个动态执行的过程。假设我们电脑上同时运行了浏览器、QQ 以及代码编辑器三个软件,这三个软件之所以同时运行,就是进程所起的作用。

线程:是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。也就是说一个进程可以包含多个线程, 因此线程也被称为轻量级进程。

2、创建线程

在 Java 中,创建线程有以下 3 种方式:

  • 1、继承 Thread 类,重写 run() 方法,该方法代表线程要执行的任务;
  • 2、实现 Runnable 接口,实现 run() 方法,该方法代表线程要执行的任务;
  • 3、实现 Callable 接口,实现 call() 方法,call() 方法作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出。

2.1 Thread 类

Thread 类是一个线程类,位于 java.lang 包下。

构造方法

Thread 类的常用构造方法如下:


  • Thread():创建一个线程对象;
  • Thread(String name):创建一个指定名称的线程对象;
  • Thread(Runnable target):创建一个基于 Runnable 接口实现类的线程对象;
  • Thread(Runnable target, String name):创建一个基于 Runnable 接口实现类,并具有指定名称的线程对象。
常用方法

public void run():线程相关的代码写在该方法中,一般需要重写;

public void start():启动当前线程;

public static void sleep(long m):使当前线程休眠 m 毫秒;

public void join():优先执行调用 join() 方法的线程。

Tips: run() 方法是一个非常重要的方法,它是用于编写线程执行体的方法,不同线程之间的一个最主要区别就是 run() 方法中的代码是不同的

实例

通过继承 Thread 类创建线程可分为以下 3 步:

  1. 定义 Thread 类的子类,并重写该类的 run() 方法。run() 方法的方法体就代表了线程要完成的任务;
  2. 创建 Thread 子类的实例,即创建线程对象;
  3. 调用线程对象的 start 方法来启动该线程。
/**
 * @author colorful@TaleLin
 */
public class ThreadDemo1 extends Thread {

    /**
     * 重写 Thread() 的方法
     */
    @Override
    public void run() {
        System.out.println("这里是线程体");
        // 当前打印线程的名称
        System.out.println(getName());
    }

    public static void main(String[] args) {
        // 实例化 ThreadDemo1 对象
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        // 调用 start() 方法,以启动线程
        threadDemo1.start();
    }

}
//运行结果:
这里是线程体
Thread-0

稍微复杂些的实例:

/**
 * @author colorful@TaleLin
 */
public class ThreadDemo2 {

    /**
     * 静态内部类
     */
    static class MyThread extends Thread {

        private int i = 3;

        MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (i > 0) {
                System.out.println(getName() + " i = " + i);
                i--;
            }
        }
    }
    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread thread1 = new MyThread("线程1");
        MyThread thread2 = new MyThread("线程2");
        // 启动线程
        thread1.start();
        thread2.start();
    }
}
//运行结果:
线程2 i = 3
线程1 i = 3
线程1 i = 2
线程2 i = 2
线程1 i = 1
线程2 i = 1

/*
代码中先启动了线程 1,再启动了线程 2 的,观察运行结果,线程并不是按照我们所预想的顺序执行的。
这里就要划重点了,不同线程,执行顺序是随机的。
如果再执行几次代码,可以观察到每次的运行结果都可能不同。
*/

2.2 Runnable 接口

为什么需要 Runnable 接口

通过实现 Runnable 接口的方案来创建线程,要优于继承 Thread 类的方案;

主要有以下原因:

  1. Java 不支持多继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 Thread 类就无法继承其它类,这不利于扩展;
  2. 继承 Thread 类通常只重写 run() 方法,其他方法一般不会重写。继承整个 Thread 类成本过高,开销过大。
实例

通过实现 Runnable 接口创建线程的步骤如下

  1. 定义 Runnable 接口的实现类,并实现该接口的 run() 方法。这个 run() 方法的方法体同样是该线程的线程执行体;
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;
  3. 调用线程对象的 start 方法来启动该线程。
/**
 * @author colorful@TaleLin
 */
public class RunnableDemo1 implements Runnable {

    private int i = 5;

    @Override
    public void run() {
        while (i > 0) {
            System.out.println(Thread.currentThread().getName() + " i = " + i);
            i--;
        }
    }

    public static void main(String[] args) {
        // 创建两个实现 Runnable 实现类的实例
        RunnableDemo1 runnableDemo1 = new RunnableDemo1();
        RunnableDemo1 runnableDemo2 = new RunnableDemo1();
        // 创建两个线程对象
        Thread thread1 = new Thread(runnableDemo1, "线程1");
        Thread thread2 = new Thread(runnableDemo2, "线程2");
        // 启动线程
        thread1.start();
        thread2.start();
    }
}
//运行结果
线程1 i = 5
线程1 i = 4
线程1 i = 3
线程1 i = 2
线程2 i = 5
线程1 i = 1
线程2 i = 4
线程2 i = 3
线程2 i = 2
线程2 i = 1

2.3 Callable 接口

为什么需要 Callable 接口

继承 Thread 类和实现 Runnable 接口这两种创建线程的方式都没有返回值。

所以,线程执行完毕后,无法得到执行结果。为了解决这个问题,Java 5 后,提供了 Callable 接口和 Future 接口,通过它们,可以在线程执行结束后,返回执行结果。

实例

通过实现 Callable 接口创建线程步骤如下

  1. 创建 Callable 接口的实现类,并实现 call() 方法。这个 call() 方法将作为线程执行体,并且有返回值;
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,这个 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值;
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  4. 调用 FutureTask 对象的 get() 方法来获得线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author colorful@TaleLin
 */
public class CallableDemo1 {

    static class MyThread implements Callable<String> {

        @Override
        public String call() { // 方法返回值类型是一个泛型,在上面 Callable<String> 处定义
            return "我是线程中返回的字符串";
        }

    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 常见实现类的实例
        Callable<String> callable = new MyThread();
        // 使用 FutureTask 类来包装 Callable 对象
        FutureTask<String> futureTask = new FutureTask<>(callable);
        // 创建 Thread 对象
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();
        // 调用 FutureTask 对象的 get() 方法来获得线程执行结束后的返回值
        String s = futureTask.get();
        System.out.println(s);
    }
}
//运行结果:
我是线程中返回的字符串

3、线程的状态和生命周期

java.lang.Thread.Starte 枚举类中定义了 6 种不同的线程状态

  • NEW:新建状态,尚未启动的线程处于此状态;
  • RUNNABLE:可运行状态,Java 虚拟机中执行的线程处于此状态;
  • BLOCK:阻塞状态,等待监视器锁定而被阻塞的线程处于此状态;
  • WAITING:等待状态,无限期等待另一线程执行特定操作的线程处于此状态;
  • TIME_WAITING:定时等待状态,在指定等待时间内等待另一线程执行操作的线程处于此状态;
  • TERMINATED:结束状态,已退出的线程处于此状态。

Tips:一个线程在给定的时间点只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

在这里插入图片描述
在这里插入图片描述

4、线程休眠

sleep() 静态方法,该方法可以使当前执行的线程睡眠(暂时停止执行)指定的毫秒数。

/**
 * @author colorful@TaleLin
 */
public class SleepDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i ++) {
            // 打印语句
            System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            try {
                // 使当前线程休眠
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // 实例化 Runnable 的实现类
        SleepDemo sleepDemo = new SleepDemo();
        // 实例化线程对象
        Thread thread = new Thread(sleepDemo);
        // 启动线程
        thread.start();
    }
}
//运行结果;
Thread-0:执行第1次
Thread-0:执行第2次
Thread-0:执行第3次
Thread-0:执行第4次
Thread-0:执行第5

Tips:不建议用sleep()方法写时钟类的东西,会越走越慢;因为程序需要获取CPU的使用权才能执行。

5、join方法

  • public final void join()
    作用:等待调用该方法的县城结束后才能执行。
  • public final void join(long millis)
    作用:等待该线程终止的最长时间为mills毫秒。

6、线程优先级

  • Java为线程类提供了10个优先级
  • 优先级可以用整数1-10表示,超过范围会抛出异常
  • 主线程默认优先级为5

优先级常量

  • MAX_PRIORITY:线程的最高优先级10
  • MIN_PRIORITY:线程的最低优先级1
  • NORM_PRIORITY:线程的默认优先级5

优先级方法

  • public int getPriority():获取线程优先级的方法
  • public void setPriority(int newPriority):设置线程优先级的方法
package com.duan.priority;

class MyThread extends Thread{
	private String name;
	public MyThread(String name){
		this.name = name;
	}
	public void run(){
		for(int i=1;i<=10;i++){
			System.out.println("线程"+name+"正在运行"+i);
		}
	}
}
public class PriorityDemo{
	public static void main(String[] args){
		//获取主线程的优先级
		int mainPrioity = Thread.currentThread().getPriority();
		System.out.println("主线程的优先级为:"+mainPriority);
		MyThread mt1 = new MyThread("线程1");
		//mt1.setPriority(10);
		mt1.setPriority(Thread.MAX_PRIORITY);
		mt1.start();
		System.out.println("线程1的优先级为:"+mt1.getPriority());
	}
}

7、线程同步

使用关键字synchronized实现

用在:成员方法、静态方法、语句块

public synchronized void saveAccount(){}

public static synchronized void saveAccount(){}

synchronized(obj){......}

8、线程间通信

  • wait() 方法:中断方法的执行,使线程等待。
  • notify() 方法:唤醒处于等待的某一个线程,使其结束等待。
  • notifyAll() 方法:唤醒所有处于等待的线程,使它们结束等待。

Tips:当生产和消费都经过wait处于阻塞状态,就出现死锁,需要notify/notifyAll来唤醒

八、输入输出流

1、什么是输入和输出(I / O)

将 Java 平台视作一个系统。当系统接收到消息时,将其称为输入,与之相反的是输出。

Java 提供了两个用于 I / O 的包:较旧的java.io包(不支持符号链接)和 较新的java.nio(“new io”)包,它对java.nio.file的异常处理进行了改进。

1.1 简单的 Java 输出——打印内容到屏幕

// 打印 Hello World,不换行
System.out.print("Hello World");
// 打印 Hello Java,并换行
System.out.println("Hello Java");

1.2 简单的 Java 输入——从键盘输入

java.util包下的Scanner类可用于获取用户从键盘输入的内容

import java.util.Scanner;

/**
 * @author colorful@TaleLin
 */
public class ScannerDemo {
    public static void main(String[] args) {
        // 创建扫描器对象
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入您的姓名:");
        // 可以将用户输入的内容扫描为字符串
        String name = scanner.nextLine();
        // 打印输出
        System.out.println("你好 ".concat(name).concat(" ,欢迎来到慕课网!"));
        // 关闭扫描器
        scanner.close();
    }
}
//运行结果:
请输入您的姓名:
Colorful
你好 Colorful ,欢迎来到慕课网!

2、什么是流(Stream)

Java 中最基本的输入/输入是使用流来完成的。

流是代表数据源和数据目标的对象;
可以读取作为数据源的流,也可以写入作为数据目标的流。
Java中的流是长度不确定的有序字节序列,它是一连串流动的字符,是以先进先出的方式发送信息的通道。

3、输入输出流的应用场景

在web产品的开发中,最常开发的功能就是上传文件到服务器了,这个文件的读写过程就要用到输入输出流。对于计算机中文件的读写复制删除等操作也都要用到输入输出流。

4、File类

java.io.File类对文件和目录进行操作。

4.1 实例化

File 类提供了如下 4 个构造方法

  1. File(File parent, String child):从父抽象路径名和子路径名字符串创建新的文件实例;
  2. File(String pathName):通过将给定的路径名字符串转换为抽象路径名,创建一个新的文件实例(最常用);
  3. File(String parent, String child):从父路径名字符串和子路径名字符串创建新的文件实例;
  4. File(URI uri):通过将给定的文件: URI转换为抽象路径名,创建一个新的文件实例。

有了目录和文件以及路径。我们分别实例化两个File对象,实例如下:

import java.io.File;

public class FileDemo1 {
    public static void main(String[] args) {
        // 传入目录绝对路径
        File dir = new File("C:\\Users\\Colorful\\Desktop\\look\\images");
        /*
        *   \\表示Windows下的路径分隔符\,
        *   Linux和MacOS下使用正斜杠/作为路径分隔符。
        *   /假设是同样的目录结构,在MacOS和Linux下是这样表示的:
        */
        
        //File dir = new File("/Users/Colorful/Desktop/duan/images");
        
        // 传入文件绝对路径
        File file = new File("C:\\Users\\Colorful\\Desktop\\look\\Hello.java");
        // 打印两个File对象
        System.out.println(dir);
        System.out.println(file);
    }
}
//运行结果:
C:\Users\Colorful\Desktop\look\images
C:\Users\Colorful\Desktop\look\Hello.java

直接打印File对象,File类重写了toString()方法,查看 Java 源码,toString()方法直接返回了getPath()实例方法,此方法返回构造方法传入的路径字符串:
在这里插入图片描述

4.2 绝对路径和相对路径

  • 绝对路径:是从盘符开始的路径
  • 相对路径:是从当前路径开始的路径

在实例化File对象时,既可以传入绝对路径,也可以传入相对路径。

示例如下目录结构:

└── look
    ├── FileDemo2.java
    ├── Hello.java
    └── images
import java.io.File;
import java.io.IOException;

public class FileDemo2 {
    public static void main(String[] args) throws IOException {
        // 传入目录相对路径
        File dir = new File(".\\images");
        File lookDir = new File("..\\look");
        // 传入文件相对路径
        File file = new File(".\\Hello.java");
    }
}

File构造方法中传入的就是相对路径,代码中的 .表示当前目录, …表示上级目录。

Tips:我们在实例化 File 对象时,不会产生对磁盘的操作,因此即使传入的文件或目录不存在,代码也不会抛出异常。
只有当调用 File 对象下的一些方法时,才会对磁盘进行操作。

File 对象下有 3 个表示路径的实例方法:

  1. String getPath():将抽象路径名转换为路径名字符串;
  2. String getAbsolute():返回此抽象路径名的绝对路径名字符串;
  3. String getCanonicalPath():返回此抽象路径名的规范路径名字符串。
import java.io.File;
import java.io.IOException;

public class FileDemo2 {
    public static void main(String[] args) throws IOException {
        // 传入目录相对路径
        File imagesDir = new File(".\\images");
        File lookDir = new File("..\\look");
        // 传入文件相对路径
        File file = new File(".\\Hello.java");
        
        System.out.println("-- imagesDir ---");
        System.out.println(imagesDir.getPath());
        System.out.println(imagesDir.getAbsolutePath());
        System.out.println(imagesDir.getCanonicalPath());

        System.out.println("-- lookDir ---");
        System.out.println(lookDir.getPath());
        System.out.println(lookDir.getAbsolutePath());
        System.out.println(lookDir.getCanonicalPath());

        System.out.println("-- file ---");
        System.out.println(file.getPath());
        System.out.println(file.getAbsolutePath());
        System.out.println(file.getCanonicalPath());
    }
}
//运行结果:
-- imagesDir ---
.\images
C:\Users\Colorful\Desktop\look\.\images
C:\Users\Colorful\Desktop\look\images
-- lookDir ---
..\imooc
C:\Users\Colorful\Desktop\look\..\imooc
C:\Users\Colorful\Desktop\look
-- file ---
.\Hello.java
C:\Users\Colorful\Desktop\look\.\Hello.java
C:\Users\Colorful\Desktop\look\Hello.java

4.3 判断对象是文件还是目录

  1. boolean isFile():测试此抽象路径名表示的文件是否为普通文件;
  2. boolean isDirectory():测试此抽象路径名表示的文件是否为目录。
import java.io.File;

public class FileDemo3 {

    public static void printResult(File file) {
        // 调用isFile()方法并接收布尔类型结果
        boolean isFile = file.isFile();
        String result1 = isFile ? "是已存在文件" : "不是已存在文件";
        // 掉用isDirectory()方法并接收布尔类型而己过
        boolean directory = file.isDirectory();
        String result2 = directory ? "是已存在目录" : "不是已存在目录";
        // 打印该file对象是否是已存在文件/目录的字符串结果
        System.out.print(file);
        System.out.print('\t' + result1 + '\t');
        System.out.println(result2);
    }

    public static void main(String[] args) {
        // 传入目录绝对路径
        File dir = new File("C:\\Users\\Colorful\\Desktop\\look\\images");
        // 传入文件绝对路径
        File file = new File("C:\\Users\\Colorful\\Desktop\\look\\test.java");
        FileDemo3.printResult(dir);
        FileDemo3.printResult(file);
    }
}
//运行结果:
C:\Users\Colorful\Desktop\look\images	不是已存在文件	是已存在目录
C:\Users\Colorful\Desktop\look\test.java	不是已存在文件	不是已存在目录
/*
代码中封装了一个静态方法printResult(),此方法打印 File 对象是否是文件/目录。
值得注意的是,磁盘中不存在C:\Users\Colorful\Desktop\look\test.java,
因此无论调用isFile()方法还是isDirectory()方法,其返回结果都为false。
*/

4.4 创建和删除目录

创建目录:

  • boolean mkdir():创建一个目录
  • boolean mkdirs():用来创建由这个抽象路径名命名的目录,包括任何必要但不存在的父目录。实际上是在递归执行mkdir()方法。
import java.io.File;

public class FileDemo4 {

    public static void main(String[] args) {
        // 传入目录绝对路径
        File dir = new File("C:\\Users\\Colorful\\Desktop\\look\\codes");
        if (!dir.exists()) {
            // 调用 mkdir() 方法
            boolean result = dir.mkdir();
            if (result) {
                System.out.println("目录创建成功");
            }
        }
    }
}
//运行结果:
目录创建成功

代码中调用了File对象的 boolean exists() 方法,此方法用于测试由此抽象路径名表示的文件或目录是否存在。当不存在时,才去创建目录。

运行代码前,look文件目录树结构如下:

└── look
    ├── FileDemo2.java
    ├── Hello.java
    └── images

运行代码后,look目录下多了一个codes目录,树结构如下:

└── look
    ├── FileDemo2.java
    ├── Hello.java
    ├── images
    └── codes

删除目录:

  • boolean delete()
import java.io.File;

public class FileDemo5 {
    public static void main(String[] args) {
        // 传入目录绝对路径
        File dir = new File("C:\\Users\\Colorful\\Desktop\\look\\codes");
        if (dir.exists()) {
            // 调用 delete() 方法
            boolean deleted = dir.delete();
            if (deleted) {
                System.out.println("删除目录成功");
            }
        }
    }
}
//运行结果:
删除目录成功

//运行代码后,树结构如下:
└── look
    ├── FileDemo2.java
    ├── Hello.java
    └── images

4.5 创建和删除文件

  • boolean createNewFile():创建一个新文件
  • boolean delete():删除文件

5、字节流

  • 二进制格式进行

5.1 InputStream 抽象类

概述

java.io.InputStream抽象类是 Java 提供的最基本的输入流,它是所有输入流的父类。

  • int read():读取输入流的下一个字节,返回的int如果为-1,则表示已经读取到文件末尾。
public abstract int read() throws IOException;

在这里插入图片描述

FileInputStream 实现类
  • FileInputStream:从文件流中读取数据。
    用于读取诸如图像数据之类的原始字节流

在这里插入图片描述

在look目录下新建一个文本文档Hello.txt,并输入内容: Hello World!

读取Hello.txt文件中数据的实例代码如下:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamDemo1 {

    public static void main(String[] args) throws IOException {
        // 实例化文件流
        FileInputStream fileInputStream = 
        new FileInputStream("C:\\Users\\Colorful\\Desktop\\look\\Hello.txt");
        for (;;) {
            int n = fileInputStream.read();
            if (n == -1) {
                // read() 方法返回-1 则跳出循环
                break;
            }
            // 将n强制转换为 char 类型
            System.out.print((char) n);
        }
        // 关闭文件流
        fileInputStream.close();
    }
}

//运行结构:
Hello World!
如果打开了一个文件并进行操作,不要忘记使用close()方法来及时关闭。这样可以让系统释放资源。

5.2 OutputStream 抽象类

概述

OutPutStream抽象类是最基本的输出流,它是所有输出流的父类。

  • void write(int b):写入一个字节到输出流。
public abstract void write(int b) throws IOException;

在这里插入图片描述

FileOutputStream 实现类
  • FileOutputStream:向文件流中写入数据。

在这里插入图片描述

向look目录下的文本文档Hello.txt输入一段字符串HHH。

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo1 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fileOutputStream = 
        new FileOutputStream("C:\\Users\\Colorful\\Desktop\\look\\Hello.txt");
        // 写入 3 个H字符
        fileOutputStream.write(72);
        fileOutputStream.write(72);
        fileOutputStream.write(72);
        fileOutputStream.close();
    }
}
//运行代码后,Hello.txt后面成功写入了 3 个字符H。

5.3 文件拷贝

package com.FileOutput.Demo;

import java.io.*;

public class FileOutputDemo1 {
    public static void main(String[] args) {
        //文件拷贝
        try{
            FileInputStream fis = new FileInputStream("happy.jpg");
            FileOutputStream fos = new FileOutputStream("happycopy.jpg");
            int n = 0;
            byte[] b = new byte[1024];
            while ((n=fis.read(b))!=-1){
                fos.write(b,0,n);
            }
            fis.close();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.4 缓冲流

  • 缓冲输入流 BufferedInputStream
  • 缓冲输出流 BufferedPutputStream
package com.file.demo;

import java.io.*;

public class BufferedDemo {
    public static void main(String[] args) {
        try{
            //将输入从数据源读取
            FileOutputStream fos = new FileOutputStream("haha.txt");
            //通过缓存流读入程序
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            FileInputStream fis = new FileInputStream("xixi.txt");
            BufferedInputStream bis = new BufferedInputStream(fis);

            //获取系统当前时间,赋值给长整型 startTime,记录开始时间
            long startTime = System.currentTimeMillis();

            bos.write(50);
            bos.write('a');
			//缓冲区强制清空(缓冲区满的情况下,自动执行写操作;不满的情况下,执行强制清空)
            bos.flush();

            System.out.println(bis.read());
            System.out.println((char) bis.read());

            //获取系统当前时间,赋值给长整型 endTime,记录结束时间
            long endTime = System.currentTimeMillis();
            System.out.println(endTime-startTime);

            fos.close();
            bos.close();
            fis.close();
            bis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6、字符流

6.1 字符输入流Reader

在这里插入图片描述

6.2 字符输出流Writer

在这里插入图片描述

6.3 字节字符转换流

  • InputStreamReader
  • OutputStreamWriter
package com.file.demo;

import java.io.*;

public class ReaderDemo {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("haha.txt");
            InputStreamReader isr = new InputStreamReader(fis,"GBK");
            //字符缓冲输入流
            BufferedReader br = new BufferedReader(isr);
            FileOutputStream fos = new FileOutputStream("xixi.txt");
            OutputStreamWriter osw = new OutputStreamWriter(fos,"GBK");
            //字符缓冲输出流
            BufferedWriter bw = new BufferedWriter(osw);
            int n =0;
            char[] cbuf = new char[10];
            while ((n=br.read(cbuf))!=-1){
                //String s = new String(cbuf,0,n);
                bw.write(cbuf,0,n);
                bw.flush();
            }
            fis.close();
            fos.close();
            osw.close();
            isr.close();
            br.close();
            bw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

九、 Java序列化与反序列化

1、序列化与反序列化

Java 语言提供自动序列化。

  • 序列化(serialize)就是将对象转换为字节流;
  • 反序列化(deserialize)就是将字节流转换为对象。

Tips:Java 序列化对象时,会把对象的状态保存成字节序列,对象的状态指的就是其成员变量,因此序列化的对象不会保存类的静态变量。

2、序列化的作用

  1. 可以在网络上传输对象字节序列;
  2. 可用于远端程序方法调用。
  3. 序列化可以将对象的字节序列存储持久化:可以将其保存在内存、文件、数据库中;

在这里插入图片描述

3、实现序列化

ObjectOutputStream类

  • void writeObject(Object obj) 方法:将一个对象写入对象输出流,也就是序列化;

ObjectInputStream类

  • Object readObject() 方法:读取一个对象到输入流,也就是反序列化。
import java.io.*;

public class SerializeDemo1 {

    static class Cat implements Serializable {
        private static final long serialVersionUID = 1L;

        private String nickname;

        private Integer age;

        public Cat() {}

        public Cat(String nickname, Integer age) {
            this.nickname = nickname;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Cat{" +
                    "nickname='" + nickname + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    /**
     * 序列化方法
     * @param filepath 文件路径
     * @param cat 要序列化的对象
     * @throws IOException
     */
    private static void serialize(String filepath, Cat cat) throws IOException {
        // 实例化file对象
        File file = new File(filepath);
        // 实例化文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        // 实例化对象输出流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        // 保存cat对象
        objectOutputStream.writeObject(cat);
        // 关闭流
        fileOutputStream.close();
        objectOutputStream.close();
    }

    /**
     * 反序列化方法
     * @param filepath 文件路径
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static void deserialize(String filepath) throws IOException, ClassNotFoundException {
        // 实例化file对象
        File file = new File(filepath);
        // 实例化文件输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        // 实例化对象输入流
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object o = objectInputStream.readObject();
        System.out.println(o);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String filename = "C:\\Users\\Colorful\\Desktop\\look\\Hello.txt";
        Cat cat = new Cat("猪皮", 1);
        serialize(filename, cat);
        deserialize(filename);
    }
}
//运行结果:
Cat{nickname='猪皮', age=1}

/*
上述代码中,定义了一个Cat类,它实现了Serializable接口,
类内部有一个private static final long serialVersionUID = 1L;,

除了Cat类的定义,还分别封装了序列化与反序列化的方法,
并在主方法中调用了这两个方法,实现了cat对象的序列化和反序列化操作。

在调用序列化方法后,磁盘中的Hello.txt文件中被cat对象写入了序列化后的数据.
*/

4、Seralizable 接口

被序列化的类必须是EnumArraySerializable中的任意一种类型。

如果要序列化的类不是枚举类型和数组类型的话,则必须实现java.io.Seralizable接口,否则直接序列化将抛出NotSerializableException异常。

4.1 serialVersionUID

serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列化时,发送方发送的和接受方接收的是可兼容的对象。
如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出 InvalidClassException。

4.2 默认序列化机制

只是让某个类实现 Serializable 接口,而没有其它任何处理的话,那么就会使用默认序列化机制。

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对其父类的字段以及该对象引用的其它对象也进行序列化。同样地,这些其它对象引用的另外对象也将被序列化。
所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

4.3 transient 关键字

当某个字段被声明为 transient 后,默认序列化机制就会忽略该字段。

将实例代码中Cat类的成员变量age声明为transient:

// 仅部分代码
static class Cat implements Serializable {
    transient private Integer age;
}

//运行程序,我们会发现成员变量age没有被序列化。

5、常用序列化工具

Java 官方的序列化存在很多缺点,因此,更倾向于使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。

Java 官方的序列化主要体现在以下方面:

  1. 性能问题:序列化后的数据相对于一些优秀的序列化的工具,还是要大不少,这大大影响存储和传输的效率;
  2. 繁琐的步骤:Java 官方的序列化一定需要实现 Serializable 接口,略显繁琐,而且需要关注 serialVersionUID;
  3. 无法跨语言使用:序列化的很大一个目的就是用于不同语言来读写数据。


列举一些其他的序列化工具:

  • thriftprotobuf - 适用于对性能敏感,对开发体验要求不高的内部系统。
  • hessian - 适用于对开发体验敏感,性能有要求的内外部系统。
  • jacksongsonfastjson - 适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值