概述

JDK,JRE

JDK( Java开发工具包)

JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了 JRE。所以安装了JDK,就不用在单独安装JRE了。 其中的开发工具:编译工具(javac.exe) 打包工具(jar.exe)等

JRE( Java运行环境)

包括Java虚拟机和Java程序所需的核心类库等, 如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

JDK、JRE、JVM关系

image-20211006182031812

JDK = JRE + 开发工具集(例如Javac编译工具等)

JRE = JVM + Java SE标准类库

面:JDK和JRE的区别是什么

Java运行时环境(JRE)是将要执行Java程序的Java虚拟机。它同时也包含了执行applet需要的浏览器插件。
Java开发工具包(JDK)是完整的Java软件开发包,包含了JRE,编译器和其他的工具(比如:JavaDoc,Java调试器),可以让开发者开发、编译、执行Java应用程序。

一个源文件中最多只能有一个public类。其它类的个数不限,如果源文件包含 一个public类,则文件名必须按该类名命名。

基础语法

关键字与保留字

image-20210318095145933 image-20210318095232153

static

1)无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。

没有对象的实例时,可以用类名.方法名()的形式访问由static修饰的类方法。

在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。

因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。

static修饰的方法不能被重写

2)单例设计模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象

因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的

3)单例模式的优点

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

饿汉式

提前创建一个当前类实例

image-20210318135202239

懒汉式

在需要的时候才创建实例

image-20210318135305604

@面:是否可以在static环境中访问非static变量

不能
static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被Java虚拟机载入的时候,会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。

@面:static关键字意思,Java中是否可以覆盖一个private或static的方法

static关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。

Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

final

@面:final关键字是怎么用的

1)final标记的不能被继承,也就是说,如果一个类你永远不会让他被继承,可以用final进行修饰。提高安全性,提高程序的可读性。final类中的成员变量可以根据需要设为final;但是final类中的所有成员方法都会被隐式地指定为final方法。

String类、System类、StringBuffer类

2)final标记的方法不能被子类重写,目的是把方法锁定,以防任何继承类修改它的含义

Object类中的getClass()

3)final标记的变量即称为常量。名称大写,且只能被赋值一次。
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

@面:final, finally, finalize的区别

final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。

package

包通常用小写单词标识。通常使用所在公司域名的倒置:com.atguigu.xxx

super

在Java类中使用super来调用父类中的指定操作:

  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可用于在子类构造器中调用父类的构造器

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

调用父类的构造器

  • 子类中所有的构造器默认都会访问父类中空参数的构造器
  • 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器。同时,只能”二选一”,即只能调用本类或者父类的构造器,且必须放在构造器的首行
  • 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错

@面:Super

1.super可以访问父类中public、default、protected修饰的成员变量,不能访问private修饰的成员变量。格式为super.成员名称。
2.super可以访问父类中public、default、protected修饰的实例方法,不能访问private修饰的实例方法。格式为super.实例方法。
3.super可以访问父类中public、default、protected修饰的构造方法,不能访问private修饰的构造方法,格式为super(参数).

image-20211007131618679

JDK中主要的包介绍

  1. java.lang

    包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能

  2. java.net

    包含执行与网络相关的操作的类和接口。

  3. java.io

    包含能提供多种输入/输出功能的类。

  4. java.util

    包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。

  5. java.text

    包含了一些java格式化相关的类

  6. java.sql

    包含了java进行JDBC数据库编程的相关类/接口

  7. java.awt

    包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 B/S C/S

命名规范

(1)包名:多单词组成时所有字母都小写:xxxyyyzzz

(2)类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz

(3)变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz

(4)常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ

变量

变量的概念:
内存中的一个存储区域
该区域的数据可以在同一类型范围内不断变化
变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值

变量的作用:
用于在内存中保存数据

使用变量注意:
Java中每个变量必须先声明,后使用
使用变量名来访问这块区域的数据
变量的作用域:其定义所在的一对{ }内,变量只有在其作用域内才有效,同一个作用域内,不能定义重名的变量

分类

image-20210329101716207

面:Java支持的数据类型,自动拆装箱

Java语言支持的8种基本数据类型是:byte、short、int、long、float、double、char、boolean
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成Integer,double转化成Double。反之就是自动拆箱。

面:字节数

byte 1字节
char 2字节
short 2字节
int 4字节
float 4字节
long 8字节
double 8字节
boolean 没有

浮点类型

float:单精度,尾数可以精确到7位有效数字。
double:双精度,精度是float的两倍。通常采用此类型。
Java 的浮点型常量默认为double型,声明float型常量,须后加‘f’或‘F’

字符类型

字符型变量的三种表现形式:
字符常量是用单引号(‘ ’)括起来的单个字符。例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’;
Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。例如:char c3 = ‘\n’; // '\n’表示换行符
直接使用 Unicode 值来表示字符型常量:‘\uXXXX’。其中,XXXX代表一个十六进制整数。如:\u000a 表示 \n。

了解:

ASCII 码

在计算机内部,所有数据都使用二进制表示。每一个二进制位(bit)有 0 和 1 两种状态,因此 8 个二进制位就可以组合出 256 种状态,这被称为一个字节(byte)。一个字节一共可以用来表示 256 种不同的状态,每一个状态对应一个符号,就是 256 个符号,从0000000 到 11111111。

ASCII码:上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码。ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

缺点:不能表示所有字符。 相同的编码表示的字符不一样:比如,130在法语编码中代表了é,在希伯来语编码中却代表(ג) 了字母Gimel

Unicode

乱码:世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。

Unicode:一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,使用 Unicode 没有乱码的问题。

Unicode 的缺点:Unicode 只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储:无法区别 Unicode 和 ASCII:计算机无法区分三个字节表示一个符号还是分别表示三个符号。另外,我们知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费。

UTF-8

UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式。

UTF-8 是一种变长的编码方式。它可以使用 1-6 个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则:

对于单字节的UTF-8编码,该字节的最高位为0,其余7位用来对字符进行编码(等同于ASCII码)。

对于多字节的UTF-8编码,如果编码包含 n 个字节,那么第一个字节的前 n 位为1,第一个字节的第 n+1 位为0,该字节的剩余各位用来对字符进行编码。在第一个字节之后的所有的字节,都是最高两位为"10",其余6位用来对字符进行编码。

boolean类型

数据只允许取值true和false,无null。 不可以使用0或非 0 的整数替代false和true。

基本数据类型转换

image-20210318102644652

byte,short,char之间不会相互转换,他们三者在计算时首先转换为int类型。

boolean类型不能与其它数据类型运算。

当把任何基本数据类型的值和字符串(String)进行连接运算时(+),基本数据类型的值将自动转化为字符串(String)类型。

字符串类型

String不是基本数据类型,属于引用数据类型

面:String是否能继承

不能,char数组用final修饰的

面:String是最基本的数据类型吗

基本数据类型包括byte、int、char、long、float、double、boolean和short。
java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类。

面:什么是值传递和引用传递

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量

引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身,所以对引用对象进行操作会同时改变原对象

一般认为,java内的传递都是值传递.

@面:在Java中如何跳出当前的多重嵌套循环

在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好),根本不能进行字符串的equals比较,否则会产生NullPointerException异常。

运算符

优先级

1645836913138

@面:&和&&的区别

&运算符有两种用法:按位与;逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&。

例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。

数组

一维数组

声明方式:type var[] 或 type[] var;

Java语言中声明数组时不能指定其长度(数组中元素的数), 例如: int a[5]; //非法

动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行

String names[];
names = new String[3];
names[0] = “钱学森”;
names[1] = “邓稼先”;
names[2] = “袁隆平”;

静态初始化:在定义数组的同时就为数组元素分配空间并赋值

int arr[] = new int[]{ 3, 9, 8};
或
int[] arr = {3,9,8};

数组元素的引用

定义并用运算符new为之分配空间后,才可以引用数组中的每个元素;

数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];

对于基本数据类型而言,默认初始化值各有不同
对于引用数据类型而言,默认初始化值为null

基本数据类型数组在显式赋值之前,Java会自动给他们赋默认值。

多维数组

image-20210514151356663

多维数组的使用

image-20210318104421339 1645837117897

数组涉及算法

二分法查找算法

//二分法查找:要求此数组必须是有序的。
int[] arr3 = new int[]{
   -99,-54,-2,0,2,33,43,256,999};
boolean isFlag = true;
int number = 256;
//int number = 25;
int head = 0;//首索引位置
int end = arr3.length - 1;//尾索引位置
while(head <= end){
   
    int middle = (head + end) / 2;
    if(arr3[middle] == number){
   
        System.out.println("找到指定的元素,索引为:" + middle);
        isFlag = false;
        break; 
    }else if(arr3[middle] > number){
   
        end = middle - 1;
    }else{
   //arr3[middle] < number
        head = middle + 1;
    } }
if(isFlag){
   
    System.out.println("未找打指定的元素");
}

排序算法

排序算法分类:内部排序和外部排序。

内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。

外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。

十大内部排序算法

选择排序:直接选择排序、堆排序
交换排序:冒泡排序、快速排序
插入排序:直接插入排序、折半插入排序、Shell排序
归并排序
桶式排序
基数排序

Arrays工具类

java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法

1645837228535

面:数组和List相互转换

1、数组转list

1)Arrays.asList(arrays)

List list = Arrays.asList(arrays);
此种方法生成的List不可进行add和remove操作,因为它是一个定长的List,跟数组长度一致

2)Collections.addAll(list, arrays);

List list = new ArrayList<>();
Collections.addAll(list, arrays);

2、List转数组

String[] arrays = list.toArray(new String[list.size()]);

面向对象编程

面:创建对象方式

Java有5种方式来创建对象:

1)使用new关键字:ObjectName obj = new ObjectName();
2)使用反射的Class类的newInstance()方法: ObjectName obj = ObjectName.class.newInstance();
3)使用反射的Constructor类的newInstance()方法:ObjectName obj=ObjectName.class.getConstructor.newInstance();
4)使用对象克隆clone()方法:ObjectName obj = obj.clone();
5)使用反序列化(ObjectInputStream)的readObject()方法:

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME))) { 
	ObjectName obj = ois.readObject(); 
}

只有new和反射用到了构造方法

@面:是否支持多继承

Java中类不支持多继承,只支持单继承(即一个类只有一个父类),但是java中的接口支持多继承,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。

@面:类和对象的区别

1.类是对某一类事物的描述,是抽象的;而对象是一个实实在在的个体,是类的一个实例。
比如:“人”是一个类,而“教师”则是“人”的一个实例。
2.对象是函数、变量的集合;而类是一组函数和变量的集合,即类是一组具有相同属性的对象集合。

@面:什么是构造函数、构造函数重载、复制构造函数

当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。
构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。
Java不支持像C++中那样的复制构造函数,因为如果你不自己写构造函数的情况下,Java不会创建默认的复制构造函数。

属性

成员变量(属性)和局部变量的区别

1645837336046

属性赋值过程

① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 通过“对象.属性“或“对象.方法”的方式赋值

方法

可变个数的形参

  1. 声明格式:方法名(参数的类型名 …参数名)
  2. 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个
  3. 可变个数形参的方法与同名的方法之间,彼此构成重载
  4. 可变参数方法的使用与方法参数部分使用数组是一致的
  5. 方法的参数部分有可变形参,需要放在形参声明的最后
  6. 在一个方法的形参位置,最多只能声明一个可变个数形参
public void test(String[] msg){
   
	System.out.println(“含字符串数组参数的test方法 ");
}
public void test1(String book){
   
	System.out.println(****与可变形参方法构成重载的test1方法****");
}
public void test1(String ... books){
   
	System.out.println("****形参长度可变的test1方法****");
}
                       
public static void main(String[] args){
   
    TestOverload to = new TestOverload();
    //下面两次调用将执行第二个test方法
    to.test1();
    to.test1("aa" , "bb");
    //下面将执行第一个test方法
    to.test(new String[]{
   "aa"});
}

方法参数的值传递机制

Java的实参值如何传入方法呢?
Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本传入方法内,而参数本身不受影响。

形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

重写

要求:

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
  • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。子类不能重写父类中声明为private权限的方法
  • 子类方法抛出的异常不能大于父类被重写方法的异常

子类重新定义了父类的方法,覆盖者可能不会限制它所覆盖的方法的访问。

注意:子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

@面:重载和重写的区别

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:发生在一个类中,两个或者是多个方法的方法名相同但是参数不同的情况。就是同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重载对返回类型没有特殊的要求,重载的方法是可以改变返回值的类型

重写:发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的方法名,参数列表和返回类型,比父类被重写的方法要更好访问,不能比父类被重写的方法声明更多的异常。

(方法的重载与重写:
对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”。
而对于重写,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。)

封装

Java权限修饰符public、protected、(缺省)、private置于类的成员定义前,用来限定对象对该类成员的访问权限。

image-20210318114726466

对于class的权限修饰只可以用public和default(缺省)。

public类可以在任意地方被访问。
default类只可以被同一个包内部的类访问。

MVC设计模式

​ MVC是常用的设计模式之一,将整个程序分为三个层次:数据模型层、视图模型层、控制器层。这种将程序数据处理、数据的展示、输入输出分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

image-20210318115719821 image-20210318115822431

多态性

对象的多态性:父类的引用指向子类的对象

Java引用变量有两个类型:

编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译,看左边;运行,看右边。

若编译时类型和运行时类型不一致,就出现了对象的多态性。多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)

虚拟方法调用

虚拟方法调用:即多态
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法,这样的方法调用在编译期是无法确定的。

Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法

编译时类型和运行时类型
编译时e为Person类型,而方法的调用是在运行时确定的,所以调用的是Student类 的getInfo()方法。

重载与重写

从编译和运行的角度看:

重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;

而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。

instanceof 操作符

x instanceof A:检验x是否为类A的对象,返回值为boolean型。

@面:面向对象的特征有哪些方面

抽象:
抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。
比如,我们要设计一个学生成绩管理系统,考察学生这个对象时,我们只关心他的班级、学号、成绩等,而不用去关心他的身高、体重这些信息。
抽象包括两个方面,一是过程抽象,二是数据抽象。
(2)继承:
继承是一种联结类的层次模型,并且允许类的重用,对象的一个新类可以从现有的类中派生,这个过程称为类继承。派生类继承了基类的特性,派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
(3)封装:
把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
(4) 多态性:
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。

Object类

Object类中的主要结构

@面:Object类的方法

Object()默认构造方法。
clone() 创建并返回此对象的一个副本。
equals(Object obj) 指示某个其他对象是否与此对象“相等”。
finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
getClass()返回一个对象的运行时类。
hashCode()返回该对象的哈希码值。 
notify()唤醒在此对象监视器上等待的单个线程。 
notifyAll()唤醒在此对象监视器上等待的所有线程。
toString()返回该对象的字符串表示。
wait()导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

==操作符与equals方法

= =:

基本类型比较值:只要两个变量的值相等,即为true。

int a=5; if(a==6){…} 

引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。

Person p1=new Person();
Person p2=new Person();
if (p1==p2){…} 

用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错

equals():

所有类都继承了Object,也就获得了equals()方法。还可以重写。

  • 只能比较引用类型,其作用与“==”相同,比较是否指向同一个对象。
  • 格式:obj1.equals(obj2)

特例:当用equals()方法进行比较时,对类File、String、Date及包装类来说,是比较类型及内容而不考虑引用的是否是同一个对象。原因:在这些类中重写了Object类的equals()方法。

当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等

任何情况下,x.equals(null),永远返回是“false”; x.equals(和x不同类型的对象)永远返回是“false”。

@面:==与equals

1)= =
基本类型比较值:只要两个变量的值相等,即为true。
引用类型比较引用:判断是否指向同一个对象,只有指向同一个对象时,才返回true。
2)equals():
所有类都继承了Object,也就获得了equals()方法,还可以重写。
只能比较引用类型,如果该方法没有被重写过,默认也是,其作用与“==”相同,比较是否指向同一个对象。通常情况下,重写equals方***比较类中的相应属性是否都相等。

@面:==比较的是什么

如果比较两个对象基于内存引用,如果两个对象的引用完全相同(指向同一个对象)时,操作将返回true,否则返回false。
如果两边是基本类型,就是比较数值是否相等。

@面:类不重写,equals()如何比较

比较是对象的地址。

toString() 方法
toString()方法在Object类中定义,其返回值是String类型,返回类名和它的引用地址。

在进行String与其它类型数据的连接操作时,自动调用toString()方法

Date now=new Date();
System.out.println(“now=”+now); 相当于
System.out.println(“now=”+now.toString()); 

可以根据需要在用户自定义类型中重写toString()方法如String 类重写了toString()方法,返回字符串的值。

s1=“hello”;
System.out.println(s1);//相当于System.out.println(s1.toString()); 

基本类型数据转换为String类型时,调用了对应包装类的toString()方法

int a=10; 
System.out.println(“a=”+a);

包装类

针对八种基本数据类型定义相应的引用类型—包装类

image-20210318124308194

1)基本类型、包装类与String类间的转换

image-20210318124622759

@面:int和Integer有什么区别

(Java是一个面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型,int的包装类就是Integer,从Java5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。)

(Java为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double)

Int是java的原始数据类型,Integer是java为int提供的封装类。引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值,对象引用实例变量的缺省值为null,而原始类型实例变量的缺省值与它们的类型有关。

main方法

由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,

//运行程序CommandPara.java
java CommandPara “Tom" “Jerry" “Shkstart"
1645853275860

代码块

1)作用:对Java类或对象进行初始化

2)代码块的分类:

一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block),没有使用static修饰的,为非静态代码块。

  • 静态代码块用static修饰的代码块
1. 可以有输出语句。
2. 可以对类的属性、类的声明进行初始化操作。
3. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
5. 静态代码块的执行要先于非静态代码块。
6. 静态代码块随着类的加载而加载,且只执行一次。
  • 非静态代码块:没有static修饰的代码块
1. 可以有输出语句。 
2. 可以对类的属性、类的声明进行初始化操作。
3. 除了调用非静态的结构外,还可以调用静态的变量或方法。
4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
5. 每次创建对象的时候,都会执行一次。且先于构造器执行。

3)static代码块通常用于初始化static的属性

class Person {
   
    public static int total;
    static {
   
        total = 100;//为total赋初值
    }
    …… //其它属性或方法声明
}
class Person {
   
    public static int total;
    static {
   
        total = 100;
        System.out.println("in static block!");
    } 
}

public class PersonTest {
   
    public static void main(String[] args) {
   
        System.out.println("total = " + Person.total);
        System.out.println("total = " + Person.total);
    } 
}

输出:
in static block
total=100
total=100

4)程序中成员变量赋值的执行顺序

- 声明成员变量的默认初始化
- 显式初始化、多个初始化块依次被执行(同级别下按先后顺序执行)
- 构造器再对成员进行初始化操作
- 通过”对象.属性”或”对象.方法”的方式,可多次给属性赋值

抽象类与抽象方法

1)有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类
2)用abstract关键字来修饰一个类,这个类叫做抽象类。 用abstract来修饰一个方法,该方法叫做抽象方法
3)含有抽象方法的类必须被声明为抽象类。
4)抽象类不能被实例化。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。
5)不能用abstract修饰变量、代码块、构造器;
6)不能用abstract修饰私有方法、静态方法、final的方法、final的类。

abstract class A {
   
    abstract void m1();
    public void m2() {
   
        System.out.println("A类中定义的m2方法");
    } 
}

class B extends A {
   
    void m1() {
   
        System.out.println("B类中定义的m1方法");
    } 
}

public class Test {
   
    public static void main(String args[]) {
   
        A a = new B();
        a.m1();
        a.m2();
    } 
}

@面:抽象类和接口有什么区别

不同点在于:
接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现很多个接口,但是只能继承一个抽象类
类可以不实现抽象类和接口声明的所有方法,在这种情况下,类也必须得声明成是抽象的。
Java接口中声明的变量默认都是final的。抽象类可以包含不是final的变量。
Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或public。
接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。

接口

1)接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个"is-a"的关系,而接口实现则是 "has-a"的关系。

如果一个抽象类中的所有方法都是抽象的,就可以将这个类用另外一种方式来定义,也就是接口定义。
接口是抽象方法和常量值的定义的集合,从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。

2)接口是抽象方法和常量值定义的集合

3)接口的特点:

- 用interface来定义。 
- 接口中的所有成员变量都默认是由【public static final】修饰的。 
- 接口中的所有抽象方法都默认是由【public abstract】修饰的。
- 接口中没有构造器。
- 接口采用多继承机制。
public interface Runner {
   
    int ID = 1;
    void start();
    public void run();
    void stop();
}

等同于

public interface Runner {
   
    public static final int ID = 1;
    public abstract void start();
    public abstract void run();
    public abstract void stop();
}
interface MyInterface{
   
    String s=MyInterface;
    public void absM1();
}
interface SubInterface extends MyInterface{
   
    public void absM2();
}
public class SubAdapter implements SubInterface{
   
    public void absM1(){
   System.out.println(“absM1”);}
    public void absM2(){
   System.out.println(“absM2”);}
}
实现类SubAdapter必须给出接口SubInterface以及父接口MyInterface中所有方法的实现。否则,SubAdapter仍需声明为abstract的。

4)接口的应用:代理模式(Proxy)

代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

image-20210318171026067

Java 8中关于接口的改进
Java 8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。
静态方法:使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到像Collection/Collections或者Path/Paths这样成对的接口和类。
默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如:java 8 API中对Collection、List、Comparator等接口提供了丰富的默认方法。

public interface AA {
   
    double PI = 3.14;
    public default void method() {
   
        System.out.println("北京");
    }
    default String method1() {
   
        return "上海";
    }
    public static void method2() {
   
        System.out.println(“hello lambda!");
    } 
}

接口中的默认方法
若一个接口中定义了一个默认方法,而另外一个接口中也定义了一个同名同参数的方法(不管此方法是否是默认方法),在实现类同时实现了这两个接口时,会出现:接口冲突。解决办法:实现类必须覆盖接口中同名同参数的方法,来解决冲突。
若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突问题。因为此时遵守:类优先原则。接口中具有相同名称和参数的默认方***被忽略。

内部类

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。

在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。

Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。

Inner class的名字不能与包含它的外部类类名相同;

1)分类:

成员内部类(static成员内部类和非static成员内部类)
局部内部类(不谈修饰符)
匿名内部类

2)成员内部类

public class Outer {
   

    private int s = 111;
    
    public class Inner {
   
        private int s = 222;
        public void mb(int s) {
   
            System.out.println(s); // 局部变量s
            System.out.println(this.s); // 内部类对象的属性s
            System.out.println(Outer.this.s); // 外部类对象属性s 
        } 
    }
    
    public static void main(String args[]) {
   
        Outer a = new Outer();
        Outer.Inner b = a.new Inner();
        b.mb(333);          
    } 
}
  • 成员内部类作为类的成员
和外部类不同,内部类还可以声明为private或protected;
可以调用外部类的结构;
内部类可以声明为static的,但此时就不能再使用外层类的非static的成员变量
  • 成员内部类作为类:
可以在内部定义属性、方法、构造器等结构;
可以声明为abstract类 ,因此可以被其它的内部类继承;
可以声明为final的;
编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)

【注意】

非static的成员内部类中的成员不能声明为static的,只有在外部类或static的成员内部类中才可声明static成员。

外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式

成员内部类可以直接使用外部类的所有成员,包括私有的数据

当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

3)局部内部类

  • 声明局部内部类
image-20210318172623880
  • 使用局部内部类

只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类
但是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口类型

  • 局部内部类的特点

内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号,以及数字编号。

只能在声明它的方法或代码块中使用,而且是先声明后使用。除此之外的任何地方都不能使用该类。

局部内部类可以使用外部类的成员,包括私有的。

局部内部类可以使用外部方法的局部变量,但是必须是final的。由局部内部类和局部变量的声明周期不同所致。

局部内部类和局部变量地位类似,不能使用public,protected,缺省,private

局部内部类不能使用static修饰,因此也不能包含静态成员

4)匿名内部类

匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。

  • 格式:
image-20210318173100826
  • 匿名内部类的特点

    匿名内部类必须继承父类或实现接口
    匿名内部类只能有一个对象
    匿名内部类对象只能使用多态形式引用
    
interface A{
   
	public abstract void fun1();
}
public class Outer{
   
	public static void main(String[] args) {
   
		new Outer().callInner(new A(){
   
			//接口是不能new但此处比较特殊是子类对象实现接口,只不过没有为对象取名
			public void fun1() {
   
				System.out.println(“implement for fun1");
			}
		});// 两步写成一步了
	}
	public void callInner(A a) {
   
		a.fun1();
	}
}       
1. 静态成员内部类:
1)静态内部类本身可以访问外部的静态资源,包括静态私有资源。但是不能访问非静态资源,可以不依赖外部类实例而实例化。  

2. 非静态成员内部类:
1)成员内部类本身可以访问外部的所有资源,但是自身不能定义静态资源,因为其实例化本身就还依赖着外部类。

3. 局部内部类:
1)局部内部类就像一个局部方法,不能被访问修饰符修饰,也不能被static修饰。
2)局部内部类只能访问所在代码块或者方法中被定义为final的局部变量。   

4. 匿名内部类:
1)没有类名的内部类,不能使用class,extends和implements,没有构造方法。
2)多用于GUI中的事件处理。
3)不能定义静态资源
4)只能创建一个匿名内部类实例。
5)一个匿名内部类一定是在new后面的,这个匿名类必须继承一个父类或者实现一个接口。
6)匿名内部类是局部内部类的特殊形式,所以局部内部类的所有限制对匿名内部类也有效。

@面:内部类可以引用包含它的类的成员吗

如果不是静态内部类,完全可以,没有什么限制
在静态内部类下,不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员

@面:对象被当作参数传递给一个方法后,是值传递还是引用传递

是值传递。Java只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。

异常处理

异常分类

**Error:**Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。

Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:空指针访问,试图读取不存在的文件,网络连接中断,数组角标越界,负数开平方根

image-20210319114102255

运行时异常

对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

@面:运行时异常与一般情况的异同

异常表示程序运行过程中可能出现的非正常状态
运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。
java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。

编译时异常

是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。

常见异常

image-20210319114459311
RuntimeException:
错误的类型转换
数组下标越界
空指针访问

IOExeption:
从一个不存在的文件中读取数据
越过文件结尾继续读取
连接一个不存在的URL

异常处理方式

1)方式一:try-catch-finally

  • 捕获异常的有关信息:

getMessage() 获取异常信息,返回字符串
printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。

image-20210319115039415
  • finally

不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。

@面:try、catch、finally中return的执行顺序

情况1:try{} catch(){}finally{} return;
程序按顺序执行。

情况2:try{ return; }catch(){} finally{} return;
程序执行try块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,最后执行try中return;
finally块之后的语句return,因为程序在try中已经return所以不再执行。

情况3:try{ } catch(){return;} finally{} return;
程序先执行try,如果遇到异常执行catch块,
有异常:则执行catch中return之前(包括return语句中的表达式运算)代码,再执行finally语句中全部代码,
最后执行catch块中return. finally之后也就是4处的代码不再执行。
无异常:执行完try再finally再return.

情况4:try{ return; }catch(){} finally{return;}
程序执行try块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,因为finally块中有return所以提前退出。

情况5:try{} catch(){return;}finally{return;}
程序执行catch块中return之前(包括return语句中的表达式运算)代码;
再执行finally块,因为finally块中有return所以提前退出。

情况6:try{ return;}catch(){return;} finally{return;}
程序执行try块中return之前(包括return语句中的表达式运算)代码;
有异常:执行catch块中return之前(包括return语句中的表达式运算)代码;
则再执行finally块,因为finally块中有return所以提前退出。
无异常:则再执行finally块,因为finally块中有return所以提前退出。

最终结论:任何执行try 或者catch中的return语句之前,【都会先执行finally语句】,如果finally存在的话。
如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,
public class IndexOutExp {
   
    public static void main(String[] args) {
   
        String friends[] = {
    "lisa", "bily", "kessy" };
        try {
   
            for (int i = 0; i < 5; i++) {
   
                System.out.println(friends[i]);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
   
            System.out.println("index err");
        }
        System.out.println("\nthis is the end");
    } 
}

程序IndexOutExp.java运行结果:java IndexOutExp
lisa
bily
kessy
index err
this is the end
public class DivideZero1 {
   
    int x;
    public static void main(String[] args) {
   
        int y;
        DivideZero1 c = new DivideZero1();
        try {
   
            y = 3 / c.x;
        } catch (ArithmeticException e) {
   
            System.out.println("divide by zero error!");
        }
        System.out.println("program ends ok!");
    } 
}

程序DivideZero1运行结果:java DivideZero1
divide by zero error!
program ends ok!

2)方式二:throws + 异常类型

如果一个方法可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理

在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

image-20210319115648060
import java.io.*;
public class ThrowsTest {
   
    public static void main(String[] args) {
   
        ThrowsTest t = new ThrowsTest();
        try {
   
            t.readFile();
        } catch (IOException e) {
   
            e.printStackTrace();
        } 
    }
    public void readFile() throws IOException {
   
        FileInputStream in = new FileInputStream("atguigushk.txt");
        int b; 
        b = in.read();
        while (b != -1) {
   
            System.out.print((char) b);
            b = in.read();
        }
        in.close();
    } 
}
image-20210319120429269

@面:如何进行异常处理,在try块中可以抛出异常吗

Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。
一般情况下是用try来执行一段程序,如果出现异常,系统会抛出一个异常,这时候你可以通过它的类型来捕捉它,或最后(finally)由缺省处理器来处理。
用try来指定一块预防所有异常的程序。紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的异常的类型。throw语句用来明确地抛出一个异常。throws用来标明一个成员函数可能抛出的各种异常。Finally为确保一段代码不管发生什么异常都被执行一段代码。

可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try语句,异常的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种”异常”进行处理,堆栈就会展开,直到遇到有处理这种异常的try语句。

@面:异常处理机制的原理以及应用

当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。违反语义规则包括2种情况。
一种是JAVA类库内置的语义检查。例如数组下标越界,会引发IndexOutOfBoundsException;访问null的对象时会引发NullPointerException。
另一种情况就是JAVA允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用throw关键字引发异常。所有的异常都是java.lang.Thowable的子类。

@面:error和exception有什么区别

error表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。
exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。

自定义异常类

  • 一般地,用户自定义异常类都是RuntimeException的子类。
  • 自定义异常类通常需要编写几个重载的构造器。
  • 自定义异常需要提供serialVersionUID
  • 自定义的异常通过throw抛出。
  • 自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。
class MyException extends Exception {
   
    static final long serialVersionUID = 13465653435L;
    private int idnumber;
    public MyException(String message, int id) {
   
        super(message);
        this.idnumber = id; 
    }
    public int getId() {
   
        return idnumber; 
    } 
}
public class MyExpTest {
   
    public void regist(int num) throws MyException {
   
        if (num < 0)
            throw new MyException("人数为负值,不合理", 3);
        else
            System.out.println("登记人数" + num);
    }
    public void manager() {
   
        try {
   
            regist(100);
        } catch (MyException e) {
   
            System.out.print("登记失败,出错种类" + e.getId());
        }
        System.out.print("本次登记操作结束");
    }
    public static void main(String args[]) {
   
        MyExpTest t = new MyExpTest();
        t.manager();
    } 
}

多线程

概念

1)程序(program)

是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

2)进程(process)

是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
程序是静态的,进程是动态的。**进程作为资源分配的单位,**系统在运行时会为每个进程分配不同的内存区域

3)线程(thread)

进程可进一步细化为线程,是一个程序内部的一条执行路径

- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

4)单核CPU和多核CPU

单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。

一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

5)并行与并发

**并行:多个CPU同时执行多个任务。**比如:多个人同时做不同的事。

**并发:一个CPU(采用时间片)同时执行多个任务。**比如:秒杀、多个人做同一件事。

6)何时需要多线程

程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
需要一些后台运行的程序时。

线程的创建和使用

1)Java语言的 JVM 允许程序运行多个线程,它通过java.lang.Thread类来体现。

2)Thread类的特性

-每个线程都是通过某个特定Thread对象的**run()方法**来完成操作的,经常把run()方法的主体称为**线程体**
-通过该Thread对象的**start()方法来启动**这个线程,而非直接调用run()

3)Thread类

image-20210319122052624

4)创建线程的方式

  • 方式一:继承Thread类
1. 定义子类继承Thread类。
2. 子类中重写Thread类中的run方法。
3. 创建Thread子类对象,即创建了线程对象。
4. 调用线程对象start方法:启动线程,调用run方法。
image-20210319150316603

注意点:

1. 如果自己手动调用run方法,那么就只是普通方法,没有启动多线程模式。
2. run方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
3. 想要启动多线程,必须调用start方法。
4. 一个线程对象只能调用一次start方法启动,若重复调用了,将抛出异常IllegalThreadStateException。

Thread类的有关方法

image-20210319152246684 image-20210319152305637
  • 方式二:实现Runnable接口(推荐)

    (1)创建一个实现了Runnable接口的类
    (2)实现类去实现Runnable中的抽象方法:run()
    (3)创建实现类的对象
    (4)将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    (5)通过Thread类的对象调用start()
    
  • 区别

    继承Thread:线程代码存放Thread子类run方法中。
    实现Runnable:线程代码存在接口的子类的run方法。
    
  • Runnable的好处

    避免了单继承的局限性
    多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
    
  • 方式三:实现Callable接口

img

与使用Runnable相比, Callable功能更强大些

a.相比run()方法,可以有返回值 
b.方法可以抛出异常 
c.支持泛型的返回值 
d.需要借助FutureTask类,比如获取返回结果

Future接口

a.可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。 
b.FutrueTask是Futrue接口的唯一的实现类 
c.FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • 方式四:使用线程池
img

@面:线程池相关API

1)ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
常用方法:
a.void execute(Runnable command) :执行任务,没有返回值,一般用来执行Runnable 
b.<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable 
c.void shutdown() :关闭连接池 

2)Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
常用方法:
a.Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池 
b.Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池 
c.Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池 
d.Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行

@面:有哪几种线程池

1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
1)工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger.MAX_VALUE), 这样可灵活的往线程池中添加线程。
2)如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间,则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
	
3、newSingleThreadExecutor创建一个单线程化的Executor,就是只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。

@面:什么是线程池

线程池就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

在工具类Executors中提供了一些静态工厂方法,生成一些常用的线程池
1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

@面:线程池的运行流程

线程池主要就是指定线程池核心线程数大小,最大线程数,存储的队列,拒绝策略,空闲线程存活时长。
当需要任务大于核心线程数时候,就开始把任务放入存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝策略,比如说记录日志,直接丢弃,或者丢弃最老的任务。

@面:线程池有什么优势

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程的优先级

image-20210319152503119

线程的生命周期

1)新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 
2)就绪:处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 
3)运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能 
4)阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态 
5)死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
img

@面:线程从创建到死亡的状态

1. 新建new:新创建了一个线程对象。

2. 就绪runnable:线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。

3. 运行running:可运行状态的线程获得了cpu时间片,执行程序代码。

4. 阻塞block:阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行状态,才有机会再次获得cpu转到运行状态。阻塞的情况分三种:
1)等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待队列中。
2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则JVM会把该线程放入锁池中。
3)其他阻塞: 运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当 sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态。

5. 死亡dead:线程run、main方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期,死亡的线程不可再次复生。

线程的同步

1.方式一:同步代码块

synchronized(同步监视器){
   //需要被同步的代码
 }

说明:

1)操作共享数据的代码,即为需要被同步的代码。
2)共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

2. 方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
关于同步方法的总结:
(1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
(2)非静态的同步方法,同步监视器是:this
   静态的同步方法,同步监视器是:当前类本身
   
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程

3.方式三:Lock锁

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

img

优先使用顺序:Lock–同步代码块(已经进入了方法体,分配了相应资源)–同步方法(在方法体之外)

@面:synchronized与Lock的异同

主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

1)Lock是显式锁,手动开启和关闭锁,synchronized是隐式锁,出了作用域自动释放
2)Lock只有代码块锁,synchronized有代码块锁和方法锁
3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

@面:关于Synchronized和lock

synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

@面:锁和同步的区别

用法上的不同:
synchronized既可以加在方法上,也可以加载特定代码块上,而lock需要显示地指定起始位置和终止位置。
synchronized是托管给JVM执行的,lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。

性能上的不同:
lock接口的实现类ReentrantLock,不仅具有和synchronized相同的并发性和内存语义,还多了超时的获取锁、定时锁、等候和中断锁等。
在竞争不是很激烈的情况下,synchronized的性能优于ReentrantLock,竞争激烈的情况下synchronized的性能会下降的非常快,而ReentrantLock则基本不变。

锁机制不同:
synchronized获取锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁。而Lock则需要开发人员手动释放,并且必须在finally中释放,否则会引起死锁。

@面:同步方法和同步代码块的区别

同步方法默认用this或者当前类class对象作为锁;

同步代码块可以选择具体以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。

@面:介绍一下Syncronized锁

synchronized修饰静态方法以及同步代码块的用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
synchronized修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。

@面:举例说明同步和异步

如果系统中存在临界资源,就是资源数量少于竞争资源的线程数量的资源。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

@面:线程同步和线程调度的相关方法

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

@面:什么是死锁

两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。

例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

@面:产生死锁的四个必要条件

1 互斥条件:
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

2 不可剥夺条件:
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

3 请求与保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

4 循环等待条件:
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

@面:处理死锁的方法

预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。

避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。

检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。

解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

@面:如何N个线程访问N个资源,又不导致死锁

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

预防死锁,预先破坏产生死锁的四个条件。互斥不可能破坏,所以有如下三种方法:
1.破坏请求和保持条件
破坏请求和保持条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。

2.破坏不可抢占条件
允许对资源实行抢夺

3.破坏循坏等待条件
破坏循环等待条件,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出,这样做就能保证系统不出现死锁。

线程的通信

1.涉及到的三个方法:

(1)**wait()😗*当前线程就进入阻塞状态,并释放同步监视器。
(2)**notify()😗*就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
(3)**notifyAll()😗*就会唤醒所有被wait的线程。

2.说明:

(1)wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
(2)wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会有IllegalMonitorStateException异常。
(3)wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

wait() 方法

  • 在当前线程中调用方法: 对象名.wait()
  • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()

  • 在当前线程中调用方法: 对象名.notify()
  • 功能:唤醒等待该对象监控权的一个/所有线程。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

@面:如何保证线程安全

通过合理的时间调度,避开共享资源的存取冲突。
在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源
设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。

@面:sleep()方法和yield()方法有什么区别

①sleep方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield方法只会给相同优先级或更高优先级的线程以运行的机会;
②线程执行sleep方法后转入阻塞状态,而执行yield方法后转入就绪状态;
③sleep方法声明抛出InterruptedException,而yield方法没有声明任何异常;
④sleep方法比yield方法具有更好的可移植性。

@面:sleep() 和 wait() 有什么区别

(1)相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
(2)不同点:
a.两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
b.调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中。
c.关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

@面:生产者消费者模式

生产者消费者问题是线程模型中的经典问题:
生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
优点:支持并发、解耦。

Java常用类

String类

1. String的特性

(1)String声明为final的,不可被继承
(2)String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小
(3)String内部定义了final char[] value用于存储字符串数据
(4)String:代表不可变的字符序列。

不可变性。体现在:
a.当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
b.当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
c.当调用String的replace()方法修改指定字符或字符串时**,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

(5)通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池中。
(6)字符串常量池中是不会存储相同内容的字符串的。

2. String对象的创建

(1)String的实例化方式

方式一:通过字面量定义的方式
方式二:通过new+构造器的方式

使用String类的构造方法创建字符串对象,如:
String s=new String(“We are Chinese");

用一个已创建的字符串创建另一个字符串,如:
String  s1=String(s);

String (char a[ ]):
用一个字符数组a创建一个字符串对象,如:
char  a[]={‘X’, ‘M’, ‘J’};
String s=new String(a); 

String (char a[],int startIndex,int count):
提取字符数组a中的一部分字符创建一个字符串对象,如:
char a[ ]={‘B’, ‘R’, ‘A’, ‘I’, ‘N’, ‘C’, ‘H’};
String s=new String(a,4,2);
1645965587869

(2) 区别

1645965638716

(3) 字符串对象存储

1645965141709 img

@面:在内存中创建了几个对象

String s = new String(“abc”); 方式创建对象,在内存中创建了几个对象?

两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:"abc"

(4)判断:

img

3. String常用方法

(1)int length():返回字符串的长度: return value.length
(2)char charAt(int index): 返回某索引处的字符return value[index]
(3)boolean isEmpty():判断是否是空字符串:return value.length == 0
(4)String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
(5)String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
(6)String trim():返回字符串的副本,忽略前导空白和尾部空白
(7)boolean equals(Object obj):比较字符串的内容是否相同
(8)boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
(9)String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+(10)int compareTo(String anotherString):比较两个字符串的大小
(11)String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
(12)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
(13)boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
(14)boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
(15)boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
(16)boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
(17)int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
(18)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
(19)int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
(20)int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
    
(21)替换:
a.String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
b.String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
c.String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
d.String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
    
(22)匹配:
a.boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
    
(23)切片:
a.String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
b.String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。

4.正则表达式

(1)正则表达式组成

由普通文本和元字符组成。其中普通文本是基本单词,元字符是语法体系。

(2)元字符

① 行的起始^与结束$

image-20210513165521347
如:
^cat$

② 字符组 […]

匹配这里列出来任何字符
如:
gr[ae]y

③ 连字符 -

表示范围
如:
[a-z]

④ 排除字符 [ ^…]

匹配出了这里列出的任何字符
如:
[^1-6]	除了1到6之外任何字符

⑤ 可选项 ?

放在一个字符后面,表示该字符可选,即可以出现也可以不出现。
如:
colou?r
可匹配color和colour

⑥ 量词 *和+

表示出现的次数
+	表示一次或多次
*	表示0次或多次

⑦ 区间词 {min,max}

表示重复出现的次数

⑧ 括号()和反向引用

括号能捕获匹配过的字符,即第一个括号为第一组,以此类推
\1,\2能记住它们,即\1表示第一组内容
image-20210513172522041
上图中的(\d)匹配一个0-9的数字字符,等价于[0-9]
\1是对(\d)的一个反向引用
即匹配,连续两个0-9的数字字符
image-20210513181811463 image-20210513181828500

⑨ 转义字符 \

image-20210513172842277

(3)匹配原则

原则1:优先选择最左端的匹配结果
原则2:标准匹配量词(*,+,?,{m,n})是匹配优先的。
image-20210513181956606

(4)调用包

java.util.regex.Pattern 
java.util.regex.Matcher

@面:Java支持正则表达式

Java中的String类提供了支持正则表达式操作的方法,包括:matches、replaceAll、replaceFirst、split。
此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作,如:
image-20210329122249071

@面:正则表达式及其用途

在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。正则表达式就是记录文本规则的代码。计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

5. String类型转换

(1)与基本数据类型转换

img

(2)与字符数组转换

img

(3)与字节数组转换

img

StringBuffer类

1、创建对象

StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器

StringBuffer()
StringBuffer(int size)
StringBuffer(String s)

length():字符个数
capacity():实际容量
String s = new String("我喜欢学习"); 
StringBuffer buffer = new StringBuffer("我喜欢学习"); 
buffer.append("数学");

2、String、StringBuffer、StringBuilder异同

三者底层都是使用char[]存储
a.String:不可变的字符序列;
b.StringBuffer:可变的字符序列;线程安全的,效率低;
c.StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;

@面:String 和StringBuffer的区别

String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。

这个String类提供了数值不可改变的字符串。而StringBuffer类提供的字符串进行修改。当知道字符数据要改变的时候你就可以使用StringBuffer。可以使用StringBuffers来动态构造字符数据。

@面:StringBuffer和StringBuilder有什么区别

StringBuffer线程安全,StringBuilder线程不安全,底层实现上的话,StringBuffer其实就是比StringBuilder多了Synchronized修饰符。

3、常用方法

a.StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
b.StringBuffer delete(int start,int end):删除指定位置的内容
c.StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
d.StringBuffer insert(int offset, xxx):在指定位置插入xxx
e.StringBuffer reverse() :把当前字符序列逆转
f.public int indexOf(String str)
g.public String substring(int start,int end):返回一个从start开始到end索引结束的左闭右开区间的子字符串
h.public int length()
i.public char charAt(int n )
j.public void setCharAt(int n ,char ch)

JDK8之前日期时间API

1.System类中的currentTimeMillis()

返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的**时间差**。称为时间戳

2.java.util.Date类

1)两个构造器的使用
a.构造器一:Date()创建一个对应当前时间的Date对象
b.构造器二:Date(long date)创建指定毫秒数的Date对象
2)两个方法的使用
a.toString():显示当前的年、月、日、时、分、秒
b.getTime():获取当前Date对象对应的毫秒数。(时间戳)

3.java.text.SimpleDateFormat类

是一个不与语言环境有关的方式来格式化和解析日期的具体类。允许进行
**格式化:日期->文本**、
**解析:文本->日期**

1)格式化:
    a.SimpleDateFormat() :默认的模式和语言环境创建对象 
    b.public SimpleDateFormat(String pattern):该构造方法可以用参数pattern 指定的格式创建一个对象,该对象调用: 
    c.public String format(Date date):方法格式化时间对象date 
2)解析: 
	a.public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。

4.java.util.Calendar(日历)类

1)Calendar是一个抽象基类,主用**用于完成日期字段之间相互操作的功能。** 
2)获取Calendar实例的方法 
    a.使用Calendar.getInstance()方法 
    b.调用它的子类GregorianCalendar的构造器。
3)一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想要的时间信息。比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND 
    a.public void set(int field,int value) 
    b.public void add(int field,int amount) 
    c.public final Date getTime() 
    d.public final void setTime(Date date) 
4)注意: 
    a.获取月份时:**一月是0,**二月是1,以此类推,12月是11 
    b.获取星期时:**周日是1,**周二是2 

JDK8中新日期时间API

1.LocalDate、LocalTime、LocalDateTime类

LocalDate、LocalTime、LocalDateTime 类是其中较重要的几个类,它们的实例是不可变的对象,分别表示使用 ISO-8601日历系统的**日期、时间、日期和时间**。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。 
1)LocalDate代表IOS格式**(yyyy-MM-dd)**的日期,可以存储 生日、纪念日等日期。 
2)LocalTime表示一个时间,而不是日期。 
3)LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。
img

2.Instant类

img

3.DateTimeFormatter 类

1)预定义的标准格式。如: 
ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME 
2)本地化相关的格式。如:
ofLocalizedDateTime(FormatStyle.LONG) 
3)自定义的格式。如:
ofPattern(“yyyy-MM-dd hh:mm:ss”)
img

比较器

1.实现对象排序的方式:

自然排序:java.lang.Comparable
定制排序:java.util.Comparator

2.方式一:自然排序

1)像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象大小的方式。

2)像String、包装类重写compareTo()方法以后,进行了从小到大的排列

3)重写compareTo(obj)的规则:
a.如果当前对象this大于形参对象obj,则返回正整数,
b.如果当前对象this小于形参对象obj,则返回负整数,
c.如果当前对象this等于形参对象obj,则返回零。

4)对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法。在compareTo(obj)方法中指明如何排序。

Comparable 的典型实现:(默认都是从小到大排列的)

String:按照字符串中字符的Unicode值进行比较
Character:按照字符的Unicode值来进行比较
数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
Boolean:true 对应的包装类实例大于 false 对应的包装类实例
Date、Time等:后面的日期时间比前面的日期时间大
class Goods implements Comparable {
   
    private String name;
    private double price;
    //按照价格,比较商品的大小
    @Override
    public int compareTo(Object o) {
   
        if(o instanceof Goods) {
   
            Goods other = (Goods) o;
            if (this.price > other.price) {
   
                return 1;
            } else if (this.price < other.price) {
   
                return -1;
            }
            return 0;
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
    //构造器、getter、setter、toString()方法略
}

public class ComparableTest{
   
    public static void main(String[] args) {
   
        Goods[] all = new Goods[4];
        all[0] = new Goods("《红楼梦》", 100);
        all[1] = new Goods("《西游记》", 80);
        all[2] = new Goods("《三国演义》", 140);
        all[3] = new Goods("《水浒传》", 120);
        Arrays.sort(all);
        System.out.println(Arrays.toString(all));
    } 
}

3.方式二:定制排序

(1)背景:

当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序

(2)重写compare(Object o1,Object o2)方法,比较o1和o2的大小:

a.如果方法返回正整数,则表示o1大于o2;
b.如果返回0,表示相等;
c.返回负整数,表示o1小于o2。

可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。

还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。

Goods[] all = new Goods[4];
all[0] = new Goods("War and Peace", 100);
all[1] = new Goods("Childhood", 80);
all[2] = new Goods("Scarlet and Black", 140);
all[3] = new Goods("Notre Dame de Paris", 120);
Arrays.sort(all, new Comparator() {
   
    @Override
    public int compare(Object o1, Object o2) {
   
        Goods g1 = (Goods) o1;
        Goods g2 = (Goods) o2;
        return g1.getName().compareTo(g2.getName());
    }
});
System.out.println(Arrays.toString(all));

4.使用对比

(1)Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小
(2)Comparator接口属于临时性的比较

@面:Comparable和Comparator接口的作用以及它们的区别

Java提供了只包含一个compareTo方法的Comparable接口。这个方法可以个给两个对象排序。具体来说,它返回负数、0、正数,来表明输入对象小于、等于、大于已经存在的对象。

Java提供了包含compare和equals两个方法的Comparator接口。
compare()方法用来给两个输入参数排序,返回负数、0、正数表明第一个参数是小于、等于、大于第二个参数。
equals()方法需要一个对象作为参数,它用来决定输入参数是否和comparator相等。只有当输入参数也是一个comparator并且输入参数和当前comparator的排序结果是相同的时候,这个方法才返回true。

System类

System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。

Math类

BigInteger与BigDecimal

StringTokenizer类

分解字符串成可被独立使用的单词,位于java.util包
1、创建对象:
StringTokenizer(String s)
StringTokenizer(String s,String delim)	
其中delim为分隔符
2、常用方法 
countTokens()		单词计数变量
hasMoreTokens()		countTokens>0 true
nextToken()  		逐个获取单词

枚举类

枚举类的属性

(1)枚举类对象的属性不应被改动, 所以应该使用 private final 修饰
(2)枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
(3)若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数

自定义枚举类

(1)**私有化类的构造器,**保证不能在类的外部创建其对象
(2)在类的内部创建枚举类的实例。声明为:public static final
(3)对象
如果有实例变量,应该声明为private final,并在构造器中初始化

img

Enum定义枚举类

1.使用说明
(1)使用 enum 定义的枚举类默认继承了java.lang.Enum类,因此不能再继承其他类
(2)枚举类的构造器只能使用 private 权限修饰符
(3)枚举类的所有实例必须在枚举类中显式列出(,分隔;结尾)。列出的实例系统会自动添加 public static final 修饰
(4)必须在枚举类的第一行声明枚举类对象

img

Enum类的主要方法

1.values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。

2.valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。

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

实现接口的枚举类

1.使用enum关键字定义的枚举类实现接口的情况

(1)情况一:实现接口,在enum类中实现抽象方法
(2)情况二:让枚举类的对象分别实现接口中的抽象方法

2.和普通 Java 类一样,枚举类可以实现一个或多个接口

3.若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。

4.若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法

注解

使用示例

1.示例一:生成文档相关的注解

2.示例二:在编译时进行格式检查(JDK内置的三个基本注解)
(1)@Override: 限定重写父类方法, 该注解只能用于方法
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
(3)@SuppressWarnings: 抑制编译器警告

3.示例三:跟踪代码依赖性,实现替代配置文件功能

自定义注解

1.参照@SuppressWarnings定义

注解声明为:@interface
内部定义成员,通常使用value表示
(3)可以指定成员的默认值,使用default定义
(4)如果自定义注解没有成员,表明是一个标识作用。

注:

如果注解有成员,在使用注解时,需要指明成员的值。
自定义注解必须配上注解的信息处理流程才有意义。
自定义注解通常都会指明两个元注解:Retention、Target

@MyAnnotation(value="尚硅谷")
public class MyAnnotationTest {
   
    public static void main(String[] args) {
   
        Class clazz = MyAnnotationTest.class;
        Annotation a = clazz.getAnnotation(MyAnnotation.class);
        MyAnnotation m = (MyAnnotation) a;
        String info = m.value();
        System.out.println(info);
    } 
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation{
   
    String value() default "auguigu"; 
}

jdk 提供的4种元注解

元注解:对现有的注解进行解释说明的注解

1.Retention:指定所修饰的在注解的生命周期:SOURCE\CLASS(默认行为)\RUNTIME。只有声明为RUNTIME生命周期的注解,才能通过反射获取。

public enum RetentionPolicy{
   
    SOURCE,
    CLASS,
    RUNTIME
}

@Retention(RetentionPolicy.SOURCE)
@interface MyAnnotation1{
    }

@Retention(RetentionPolicy.RUNTIME) 
@interface MyAnnotation2{
    }

**2.Target:**用于指定被修饰的 Annotation 能用于修饰哪些程序元素@Target 也包含一个名为 value 的成员变量。

1646015449887

**3.Documented:表示所修饰的注解在被javadoc解析时,保留下来。**定义为Documented的注解必须设置Retention值为RUNTIME

4.Inherited:被它修饰的 Annotation 将具有继承性。

jdk8中注解的新特性

可重复注解、类型注解

1.可重复注解:
在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class
MyAnnotation的Target和Retention等元注解与MyAnnotations相同。

1646015581782

2.类型注解:
(1)ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。
(2)ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。

public class TestTypeDefine<@TypeDefine() U> {
   
    private U u;
    public <@TypeDefine() T> void test(T t){
   
    } 
}

@Target({
   ElementType.TYPE_PARAMETER})
@interface TypeDefine{
    }
@MyAnnotation
public class AnnotationTest<U> {
   
    @MyAnnotation
    private String name;
    public static void main(String[] args) {
   
        AnnotationTest<@MyAnnotation String> t = null;
        int a = (@MyAnnotation int) 2L;
        @MyAnnotation
        int b = 10;
    }
    public static <@MyAnnotation T> void method(T t) {
    }
    public static void test(@MyAnnotation String arg) throws @MyAnnotation Exception {
    } 
}

@Target(ElementType.TYPE_USE)
@interface MyAnnotation {
    }

Java集合

概述

1.集合、数组都是对多个数据进行存储操作的结构,简称Java容器。

说明:此时的存储,**主要指的是内存层面的存储,**不涉及到持久化的存储

2.数组在存储多个数据方面的特点:

1)一旦初始化以后,其长度就确定了。
2)数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。比如:String[] arr;int[] arr1;Object[] arr2;

3.数组在存储多个数据方面的缺点:

1)一旦初始化以后,其长度就不可修改。
2)数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
3)获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
4)数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

集合框架

Java集合可分为 Collection 和 Map 两种体系

Collection接口:单列数据,定义了存取一组对象的方法的集合
Map接口:双列数据,保存具有映射关系“key-value对”的集合

1.Collection接口

单列集合,用来存储一个一个的对象,Collection又可分为:

(1)List接口:存储有序的、可重复的数据。
ArrayList、LinkedList、Vector

(2)Set接口:存储无序的、不可重复的数据
HashSet、LinkedHashSet、TreeSet
img

Collection 接口方法

1、添加 
add(Object obj) 
addAll(Collection coll) 
2、获取有效元素的个数 
int size() 
3、清空集合 
void clear() 
4、是否是空集合 
boolean isEmpty() 
5、是否包含某个元素 
boolean contains(Object obj):是通过元素的equals方法来判断是否 
是同一个对象 
boolean containsAll(Collection c):也是调用元素的equals方法来比 
6、删除 
boolean remove(Object obj) :通过元素的equals方法判断是否是 
要删除的那个元素。只会删除找到的第一个元素 
boolean removeAll(Collection coll):取当前集合的差集 
7、取两个集合的交集 
 boolean retainAll(Collection c):把交集的结果存在当前集合中,不 影响c 
8、集合是否相等 
boolean equals(Object obj) 
9、转成对象数组 
Object[] toArray() 
10、获取集合对象的哈希值 
hashCode() 
11、遍历 
iterator()

2.Map接口:

双列集合,用来存储一对(key - value)的数据
HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
img

@面:List、Set、Map是否继承自Collection接口

List、Set是,Map不是。
Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素,类似数学中的集合,List是线性结构的容器,适用于按数值索引访问元素的情形。

@面:List、Map、Set各有什么特点

List以特定索引来存取元素,可以有重复元素。
Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。
Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。
Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

@面:讲讲你所知道的常用集合类以及主要方法

最常用的集合类是List和Map。
List的具体实现包括ArrayList和Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。List适用于按数值索引访问元素的情形。
Map提供了一个更通用的元素存储方法。Map集合类用于存储键值对元素,其中每个键映射到一个值。

@面:Collection 和 Collections的区别

Collection是集合类的上级接口,继承与他的接口主要有Set和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

@面:Java集合类框架的基本接口有哪些

集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。

Java集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。
Java集合类里面最基本的接口有:
Collection:代表一组对象,每一个对象都是它的子元素。
    Set:不包含重复元素的Collection。
    List:有顺序的collection,并且可以包含重复元素。

Map:可以把键映射到值的对象,键不能重复。

Collection体系

分为List接口和Set接口


  • List接口

存储有序的、可重复的数据。 -->“动态”数组,替换原有的数组

1)ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
2)LinkedList:对于频繁的插入、删除操作,线程不安全的,使用此类效率比ArrayList高;底层使用双向链表存储
3)Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储
ArrayList、LinkedList、Vector三者的相同点:
三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据

List接口方法

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

(1)void add(int index, Object ele):在index位置插入ele元素
(2)boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
(3)Object get(int index):获取指定index位置的元素
(4)int indexOf(Object obj):返回obj在集合中首次出现的位置
(5)int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
(6)Object remove(int index):移除指定index位置的元素,并返回此元素
(7)Object set(int index, Object ele):设置指定index位置的元素为ele
(8)List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

@面:ArrayList,Vector,LinkedList的存储性能和特性

ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据,以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法,线程安全,通常性能上较ArrayList差。

LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。

@面:ArrayList和LinkedList的异同

- ArrayList和LinkedList都实现了List接口,二者都线程不安全,相对于线程安全的Vector,执行效率高。
- ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
- 对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
- 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
- LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

@面:ArrayList和Vector的区别

Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类,属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

@面:数组(Array)和列表(ArrayList)的区别

Array和ArrayList的不同点:
- Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
- Array大小是固定的,ArrayList的大小是动态变化的。
- ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
- 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

@面:说明ArrayList是否会越界

ArrayList并发add()可能出现数组下标越界异常。

ArrayList

image-20210514151845691

1)jdk 7情况下

ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

2)jdk 8中ArrayList的变化:

ArrayList list = new ArrayList();
//底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123);
//第一次调用add()时,底层才创建了长度10的数组,延迟了数组的创建,节省内存并将数据123添加到elementData[0]
后续的添加和扩容操作与jdk7无异。

总结:常用方法

(1)增:add(Object obj)
(2)删:remove(int index) / remove(Object obj)
(3)改:set(int index, Object ele)
(4)查:get(int index)
(5)插:add(int index, Object ele)
(6)长度:size()
(7)遍历:①Iterator迭代器方式 ②增强for循环 ③普通的循环

LinkedList

对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高

新增方法:
void addFirst(Object obj) 
void addLast(Object obj)
Object getFirst()
Object getLast()
Object removeFirst()
Object removeLast()

LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:prev变量记录前一个元素的位置;next变量记录下一个元素的位置

private static class Node<E> {
   
    E item;
    Node<E> next;
    Node<E> prev;
    Node(Node<E> prev, E element, Node<E> next) {
   
        this.item = element;
        this.next = next;
        this.prev = prev;
    } 
}

  • Set接口

存储无序的、不可重复的数据

1)HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值;底层是数组+链表的结构。
	LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历,对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
2)TreeSet:可以按照添加对象的指定属性,进行排序。

Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败。Set判断两个对象是否相同不是使用 == 运算符,而是根据equals()方法。

要求:

向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()方法,以实现对象相等规则。即:相等的对象必须具有相等的散列码

重写两个方法的小技巧:对象中用作equals()方法比较的方法属性,都应该用来计算hashCode值。

@面:Object若不重写hashCode()的话,hashCode()如何计算出来的

Object的hashcode方法是本地方法,也就是用c语言或c++实现的,该方法直接返回对象的内存地址。

HashSet

1)特点:

  • 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
  • 不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。
  • HashSet 不是线程安全的
  • 集合元素可以是 null

2)判断两个元素相等的标准

两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等

3)添加元素的过程:

image-20210329140813717
向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即先通过哈希值计算元素位置),即索引位置,判断
数组此位置上是否已经有元素:
        如果此位置上没有其他元素,则元素a添加成功。 --->情况1
        如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值(即若元素放在相同位置时,要先比较哈希值):
            如果hash值不相同,则元素a添加成功。--->情况2
            如果hash值相同,进而需要调用元素a所在类的equals()方法:
                   equals()返回true,元素a添加失败
                   equals()返回false,则元素a添加成功。--->情况3

对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下

重写 hashCode() 方法的基本原则

在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

重写 equals() 方法的基本原则

以自定义的Customer类为例,何时需要重写equals()?

当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。
因此,违反了“相等的对象必须具有相等的散列码”。
结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

LinkedHashSet

LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
img

TreeSet

TreeSet 可以确保集合元素处于排序状态,TreeSet底层使用红黑树结构存储数据

1)向TreeSet中添加的数据,要求是相同类的对象。
2)两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)
3)自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals().
4)定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals().

Map体系

Map:双列数据,存储key-value对的数据

img

Map结构的理解

1)Map中的key:无序的、不可重复的,使用Set存储所有的key---> key所在的类要重写equals()和hashCode() (以HashMap为例)
2)Map中的value:无序的、可重复的,使用Collection存储所有的value --->value所在的类要重写equals()
3)Map中的enty:无序的、不可重复的,key-value构成了一个Entry对象,使用Set存储所有的entry。

HashMap 判断两个key相等的标准是:两个key通过 equals() 方法返回true,hashCode值也相等。
HashMap 判断两个value相等的标准是:两个value通过 equals() 方法返回true。

1、HashMap:

作为Map的主要实现类;线程不安全的,效率高;存储null的key和value;
Map map = new HashMap();
//map.put(..,..)省略
System.out.println("map的所有key:");
Set keys = map.keySet();// HashSet
for (Object key : keys) {
   
    System.out.println(key + "->" + map.get(key));
}

System.out.println("map的所有的value:");
Collection values = map.values();
Iterator iter = values.iterator();
while (iter.hasNext()) {
   
    System.out.println(iter.next());
}

System.out.println("map所有的映射关系:");
// 映射关系的类型是Map.Entry类型,它是Map接口的内部接口
Set mappings = map.entrySet();
for (Object mapping : mappings) {
   
    Map.Entry entry = (Map.Entry) mapping;
    System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
}

数组+链表(jdk7及之前)

image-20210329141807727

HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

添加元素的过程:

向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值相同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

HashMap的扩容

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)×loadFactor 时 ,就会进行数组 扩容 ,loadFactor的默认值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16×0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

数组+链表+红黑树(jdk 8)

image-20210329141833796

HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表
中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象
可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

那么HashMap什么时候进行扩容和树形化呢?

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size) * loadFactor 时 , 就会进行数组扩容 , loadFactor 的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16 * 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

jdk7在底层实现方面:

HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
	如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
	如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比:调用key1所在类的equals(key2)方法,比较:
		如果equals()返回false:此时key1-value1添加成功。----情况3
		如果equals()返回true:使用value1替换value2。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。
默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

jdk8 相较于jdk7在底层实现方面的不同:

1. new HashMap():底层没有创建一个长度为16的数组
2. jdk8底层的数组是:Node[],而非Entry[]
3. 首次调用put()方法时,底层创建长度为16的数组
4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
   4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
   4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
image-20210329152749096
hashcode的值用于计算在数组中下表
equals用于计算查找的key是否等于key1、key2...

@面:为什么重写equals还要重写hashcode

HashMap中,如果要比较key是否相等,要同时使用这两个函数,因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。

HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。

如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。HashMap用来判断key是否相等的方法,其实是调用了HashSet判断加入元素是否相等。

重载hashCode()是为了对同一个key,能得到相同HashCode,这样HashMap就可以定位到我们指定的key上。
重载equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。

@面:hashCode()和equals()方法有什么联系

➀相等的对象必须具有相等的哈希码(或者散列码)。
➁如果两个对象的hashCode相同,它们并不一定相同。

@面:两个对象值相同(x.equals(y) == true),但却可有不同的hash code,该说法是否正确

不对
如果两个对象x和y满足x.equals(y) == true,它们的哈希码应当相同。
Java对于eqauls方法和hashCode方法是这样规定的:
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
(2)如果两个对象的hashCode相同,它们并不一定相同。
当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
原因:
在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。

2、TreeMap:

保证按照添加的key-value对进行排序,实现排序遍历,线程不安全的。此时考虑key的自然排序或定制排序,底层使用红黑树

TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。

TreeSet底层使用红黑树结构存储数据

TreeMap 的 Key 的排序:

自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口

TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

3、Hashtable:

作为古老的实现类;线程安全的,效率低;不能存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型

Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

@面:HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,因此很多特性非常相似。
但是,他们有以下不同点:
1)HashMap允许键和值是null,而Hashtable不允许键或者值是null。
2)Hashtable是同步的,而HashMap不是,因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。
3)HashMap提供了可供应用迭代的键的集合,因此,HashMap是快速失败的。另一方面,Hashtable提供了对键的列举。

@面:解释一下TreeMap

TreeMap是一个有序的key-value集合,基于红黑树的NavigableMap实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。
TreeMap的特性:
根节点是黑色
每个节点都只能是红色或者黑色
每个叶节点是黑色的。
如果一个节点是红色的,则它两个子节点都是黑色的,也就是说在一条路径上不能出现两个红色的节点。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

@面:TreeMap的底层实现

TreeMap的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证快速检索指定节点。

红黑树的插入、删除、遍历时间复杂度都为O(lgN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键的值的大小有序输出。红黑树性质:
性质1:每个节点要么是红色,要么是黑色。
性质2:根节点永远是黑色的。
性质3:所有的叶节点都是空节点(即 null),并且是黑色的。
性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

@面:map的分类和常见的情况

java为数据结构中的映射定义了一个接口java.util.Map。
它有四个实现类,分别是HashMap、Hashtable、LinkedHashMap、TreeMap.
Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。

1)
Hashmap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。 
HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null
HashMap不支持线程的同步,任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

2)
Hashtable与HashMap类似,它继承自Dictionary类
不同的是:它不允许记录的键或者值为空,它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时会比较慢。

3)
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

4)
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

一般情况下,我们用的最多的是HashMap,在Map中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap更好。如果需要输出的顺序和输入的相同,那用LinkedHashMap可以实现,它还可以按读取顺序来排列.

Map方法

(1)添加、删除、修改操作:
a.Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
b.void putAll(Map m):将m中的所有key-value对存放到当前map中
c.Object remove(Object key):移除指定key的key-value对,并返回value
d.void clear():清空当前map中的所有数据
    
(2) 元素查询的操作:
a.Object get(Object key):获取指定key对应的value
b.boolean containsKey(Object key):是否包含指定的key
c.boolean containsValue(Object value):是否包含指定的value
d.int size():返回map中key-value对的个数
e.boolean isEmpty():判断当前map是否为空
f.boolean equals(Object obj):判断当前map和参数对象obj是否相等
    
(3) 元视图操作的方法:
a.Set keySet():返回所有key构成的Set集合
b.Collection values():返回所有value构成的Collection集合
c.Set entrySet():返回所有key-value对构成的Set集合

常用方法

1.添加:put(Object key,Object value)
2.删除:remove(Object key)
3.修改:put(Object key,Object value)
4.查询:get(Object key)
5.长度:size()
6.遍历:keySet() / values() / entrySet()

Iterator接口

Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。 
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。 
Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。 
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

1.Iterator接口的方法

img

@面:Iterator和ListIterator的区别

Iterator和ListIterator的区别是:
1)Iterator可用来遍历List和Set集合,但是ListIterator只能用来遍历List。
2)Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
3)ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引。

2.迭代器的执行原理

img

@面:什么是迭代器

Iterator提供了统一遍历操作集合元素的统一接口, Collection接口实现Iterable接口。
每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作。
在迭代元素的时候不能通过集合的方法删除元素,否则会抛出ConcurrentModificationException异常. 但是可以通过Iterator接口中的remove()方法进行删除。

foreach循环遍历

1.Java5.0提供了foreach循环迭代访问Collection和数组。 
2.遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。 
3.遍历集合的底层调用Iterator完成操作。 
4.foreach还可以用来遍历数组。
img

Collections工具类

是操作CollectionMap的工具类
    
1.排序操作:(均为static方法) 
(1)reverse(List):反转 List 中元素的顺序 
(2)shuffle(List):对 List 集合元素进行随机排序 
(3)sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序 
(4)sort(ListComparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 
(5)swap(Listintint):将指定 list 集合中的 i 处元素和 j 
    
2.查找、替换 
(1)Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素 
(2)Object max(CollectionComparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素 
(3)Object min(Collection) 
(4)Object min(CollectionComparator) 
(5)int frequency(CollectionObject):返回指定集合中指定元素的出现次数 
(6)void copy(List dest,List src):将src中的内容复制到dest中 
(7)boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
img

泛型

概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实
际的类型参数,也称为类型实参)。

@面:什么是泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
那么参数化类型怎么理解呢?
就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

自定义泛型

泛型的声明

interface List 和 class GenTest<K,V>
其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写。

泛型的实例化

一定要在类名后面指定类型参数的值(类型)。如:

List<String> strList = new ArrayList<String>();
Iterator<Customer> iterator = customers.iterator();

T只能是类,不能用基本数据类型填充。但可以使用包装类填充
把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想

泛型类、泛型接口

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>

  2. 泛型类的构造器如下:public GenericClass(){}。

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  4. 泛型不同的引用不能相互赋值。尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。

  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。

  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  7. jdk1.7,泛型的简化操作:ArrayList flist = new ArrayList<>();

  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。

class GenericTest {
   
    public static void main(String[] args) {
   
        // 1、使用时:类似于Object,不等同于Object
        ArrayList list = new ArrayList();
        // list.add(new Date());//有风险
        list.add("hello");
        test(list);// 泛型擦除,编译不会类型检查
        
        // ArrayList<Object> list2 = new ArrayList<Object>();
        // test(list2);//一旦指定Object,编译会类型检查,必须按照Object处理
    }
    public static void test(ArrayList<String> list) {
   
        String str = "";
        for (String s : list) {
   
            str += s + ","; }
        System.out.println("元素:" + str);
    } 
}
  1. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。

  2. 异常类不能是泛型的

  3. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];

  4. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现。

      • 没有类型擦除;
      • 具体类型
    • 子类保留父类的泛型:泛型子类。

      • 全部保留;
      • 部分保留

    结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

class Father<T1, T2> {
   
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {
   // 等价于class Son extends Father<Object,Object>{
   
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
   
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
   
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
   
}
class Father<T1, T2> {
   
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{
   //等价于class Son extends Father<Object,Object>{
   
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
   
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
   
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
   
}

泛型方法

方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。

泛型方法的格式:

[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
public class DAO {
   
    public <E> E get(int id, E e) {
   
        E result = null;
        return result; 
    } 
}
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
   
    for (T o : a) {
   
        c.add(o);
    } 
}

public static void main(String[] args) {
   
    Object[] ao = new Object[100];
    Collection<Object> co = new ArrayList<Object>();
    fromArrayToCollection(ao, co);
    String[] sa = new String[20];
    Collection<String> cs = new ArrayList<>();
    fromArrayToCollection(sa, cs);
    Collection<Double> cd = new ArrayList<>();
    // 下面代码中T是Double类,但sa是String类型,编译错误。
    // fromArrayToCollection(sa, cd);
    // 下面代码中T是Object类型,sa是String类型,可以赋值成功。
    fromArrayToCollection(sa, co);
}

通配符

1.使用类型通配符:?
比如:List<?> ,Map<?,?>
List<?>是List、List等各种泛型List的父类。

2.读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。

3.写入list中的元素时

将任意元素加入到其中不是类型安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误

因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
唯一的例外的是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。

public static void main(String[] args) {
   
    List<?> list = null;
    list = new ArrayList<String>();
    list = new ArrayList<Double>();
    // list.add(3);//编译不通过
    list.add(null);
    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    l1.add("尚硅谷");
    l2.add(15);
    read(l1);
    read(l2);
}
public static void read(List<?> list) {
   
    for (Object o : list) {
   
        System.out.println(o);
    } 
}

注意点

//注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
}
//注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
}
//注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();

有限制的通配符

  • <?>

允许所有泛型的引用调用

  • 通配符指定上限

上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=

  • 通配符指定下限

下限super:使用时指定的类型不能小于操作的类,即>=

举例:

<? extends Number> (无穷小 , Number]
只允许泛型为Number及Number子类的引用调用

<? super Number> [Number , 无穷大) 
只允许泛型为Number及Number父类的引用调用

<? extends Comparable>
只允许泛型为实现Comparable接口的实现类的引用调用
public static void printCollection3(Collection<? extends Person> coll) {
   
    //Iterator只能用Iterator<?>或Iterator<? extends Person>.why?
    Iterator<?> iterator = coll.iterator();
    while (iterator.hasNext()) {
   
        System.out.println(iterator.next());
    } 
}

public static void printCollection4(Collection<? super Person> coll) {
   
    //Iterator只能用Iterator<?>或Iterator<? super Person>.why?
    Iterator<?> iterator = coll.iterator();
    while (iterator.hasNext()) {
   
        System.out.println(iterator.next());
    } 
}
public static void main(String[] args) {
   
    HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>();
    ArrayList<Citizen> list = new ArrayList<Citizen>();
    list.add(new Citizen("刘恺威"));
    list.add(new Citizen("杨幂"));
    list.add(new Citizen("小糯米"));
    map.put("刘恺威", list);
    
    Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet();
    Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator();
    while (iterator.hasNext()) {
   
        Entry<String, ArrayList<Citizen>> entry = iterator.next();
        String key = entry.getKey();
        ArrayList<Citizen> value = entry.getValue();
        System.out.println("户主:" + key);
        System.out.println("家庭成员:" + value);
    } 
}

IO流

File类常用方法

1.File类的获取功能

(1)public String getAbsolutePath():获取绝对路径 
(2)public String getPath() :获取路径 
(3)public String getName() :获取名称 
(4)public String getParent():获取上层文件目录路径。若无,返回null 
(5)public long length() :获取文件长度(即:字节数)。不能获取目录的长度。 
(6)public long lastModified() :获取最后一次的修改时间,毫秒值 
(7)public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组 
(8)public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File

2.File类的重命名功能

(1)public boolean renameTo(File dest):把文件重命名为指定的文件路径

3.File类的判断功能

(1)public boolean isDirectory():判断是否是文件目录 
(2)public boolean isFile() :判断是否是文件 
(3)public boolean exists() :判断是否存在 
(4)public boolean canRead() :判断是否可读 
(5)public boolean canWrite() :判断是否可写 
(6)public boolean isHidden() 

4.File类的创建功能

(1)public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false 
(2)public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。 
(3)如果此文件目录的上层目录不存在,也不创建。 
(4)public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建 
(5)注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目 
(6)路径下。 

5.File类的删除功能

(1)public boolean delete():删除文件或者文件夹 
(2)删除注意事项: 
(3)Java中的删除不走回收站。 
(4)要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

流的分类

1.操作数据单位:字节流、字符流
2.数据的流向:输入流、输出流
3.流的角色:节点流、处理流

img

对于文本文件(.txt,.java,.c,.cpp),使用字符流处理;对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,…),使用字节流处理

@面:有几种类型的流

字节流,字符流。
字节流继承于InputStream OutputStream
字符流继承于InputStreamReader OutputStreamWriter
在java.io包中还有许多其他的流,主要是为了提高性能和使用方便。

节点流和处理流

节点流:直接从数据源或目的地读写数据

image-20211008150651672

处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

image-20211008150714023

InputStream & Reader

InputStream 和 Reader 是所有输入流的基类。

InputStream(典型实现:FileInputStream)

int read()
从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。 

int read(byte[] b)
从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。 

int read(byte[] b, int off,int len)
将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。

public void close() throws IOException
关闭此输入流并释放与该流关联的所有系统资源。

Reader(典型实现:FileReader)

int read()
读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1 

int read(char[] cbuf)
将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。 

int read(char[] cbuf,int off,int len)
将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。 

public void close() throws IOException
关闭此输入流并释放与该流关联的所有系统资源。

程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源。

FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader

OutputStream & Writer

OutputStream

void write(int b)
将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。 

void write(byte[] b)
将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同。 

void write(byte[] b,int off,int len)
将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。 

public void flush()throws IOException
刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。 

public void close() throws IOException
关闭此输出流并释放与该流关联的所有系统资源。

Writer

void write(int c)
写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。 

void write(char[] cbuf)
写入字符数组。 

void write(char[] cbuf,int off,int len)
写入字符数组的某一部分。从off开始,写入len个字符

void write(String str)
写入字符串。 

void write(String str,int off,int len)
写入字符串的某一部分。 

void flush()
刷新该流的缓冲,则立即将它们写入预期目标。 

public void close() throws IOException
关闭此输出流并释放与该流关联的所有系统资源

因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数

void write(String str);
void write(String str, int off, int len);

FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter

流的体系结构

img img

节点流

读取文件

建立一个流对象,将已存在的一个文件加载进流。
FileReader fr = new FileReader(new File(“Test.txt”));

创建一个临时存放数据的数组。
char[] ch = new char[1024];

调用流对象的读取方法将流中的数据读入到数组中。
fr.read(ch);

关闭资源。
fr.close();

FileReader fr = null;
try {
   
    fr = new FileReader(new File("c:\\test.txt"));
    char[] buf = new char[1024];
    int len;
    while ((len = fr.read(buf)) != -1) {
   
        System.out.print(new String(buf, 0, len));
    }
} catch (IOException e) {
   
    System.out.println("read-Exception :" + e.getMessage());
} finally {
   
    if (fr != null) {
   
        try {
   
            fr.close();
        } catch (IOException e) {
   
            System.out.println("close-Exception :" + e.getMessage());
        } 
    } 
}

写入文件

1.创建流对象,建立数据存放文件
FileWriter fw = new FileWriter(new File(“Test.txt”));
2.调用流对象的写入方法,将数据写入流
fw.write(“atguigu-songhongkang”);
3.关闭流资源,并将流中的数据清空到文件中。
fw.close();

FileWriter fw = null;
try {
   
    fw = new FileWriter(new File("Test.txt"));
    fw.write("atguigu-songhongkang");
} catch (IOException e) {
   
    e.printStackTrace();
} finally {
   
    if (fw != null)
        try {
   
            fw.close();
        } catch (IOException e) {
   
            e.printStackTrace();
        } 
}

注意点

定义文件路径时,注意:可以用“/”或者“\”。

在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。

如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖,在文件内容末尾追加内容。

在读取文件时,必须保证该文件已存在,否则报异常。

字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt

字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。

缓冲流

1.缓冲流:

在使用这些流类时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)的缓冲区。

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为

**(1)BufferedInputStream**
**(2)BufferedOutputStream**
**(3)BufferedReader**
**(4)BufferedWriter**

2.作用:

**提供流的读取、写入的速度**
提高读写速度的**原因:内部提供了一个缓冲区**

当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。

向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流
关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流
flush()方法的使用:手动将buffer中内容写入文件
如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出

img
BufferedReader br = null;
BufferedWriter bw = null;
try {
   
    // 创建缓冲流对象:它是处理流,是对节点流的包装
    br = new BufferedReader(new FileReader("d:\\IOTest\\source.txt"));
    bw = new BufferedWriter(new FileWriter("d:\\IOTest\\dest.txt"));
    String str;
    while ((str = br.readLine()) != null) {
    // 一次读取字符文本文件的一行字符
        bw.write(str); // 一次写入一行字符串
        bw.newLine(); // 写入行分隔符
    }
    bw.flush(); // 刷新缓冲区
} catch (IOException e) {
   
    e.printStackTrace();
} finally {
   
    // 关闭IO流对象
    try {
   
        if (bw != null) {
   
            bw.close(); // 关闭过滤流时,会自动关闭它所包装的底层节点流
        }
    } catch (IOException e) {
   
        e.printStackTrace();
    }
    try {
   
        if (br != null) {
   
            br.close();
        }
    } catch (IOException e) {
   
        e.printStackTrace();
    } 
}

转换流

转换流提供了在字节流和字符流之间的转换

1.转换流:属于字符流

InputStreamReader:将一个字节的输入流转换为字符的输入流

实现将字节的输入流按指定字符集转换为字符的输入流。
需要和InputStream“套接”。
构造器

public InputStreamReader(InputStream in)
public InputSreamReader(InputStream in,String charsetName)
如: Reader isr = new InputStreamReader(System.in,”gbk”);

OutputStreamWriter:将一个字符的输出流转换为字节的输出流

实现将字符的输出流按指定字符集转换为字节的输出流。
需要和OutputStream“套接”。
构造器

public OutputStreamWriter(OutputStream out)
public OutputSreamWriter(OutputStream out,String charsetName)
img
public void testMyInput() throws Exception {
   
    FileInputStream fis = new FileInputStream("dbcp.txt");
    FileOutputStream fos = new FileOutputStream("dbcp5.txt");
    InputStreamReader isr = new InputStreamReader(fis, "GBK");
    OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
    BufferedReader br = new BufferedReader(isr);
    BufferedWriter bw = new BufferedWriter(osw);
    String str = null;
    while ((str = br.readLine()) != null) {
   
        bw.write(str);
        bw.newLine();
        bw.flush();
    }
    bw.close();
    br.close();
}

2.作用:

**提供字节流与字符流之间的转换**

3.**解码:**字节、字节数组 —>字符数组、字符串
**编码:**字符数组、字符串 —> 字节、字节数组

4.字符集

(1)ASCII:美国标准信息交换码。用一个字节的7位可以表示。
(2)ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。
(3)GB2312:中国的中文编码表。最多两个字节编码所有字符
(4)GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
(5)Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
(6)UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

标准输入、输出流

System.in:标准的输入流,默认从键盘输入
System.out:标准的输出流,默认从控制台输出
System类的setIn(InputStream is) / setOut(PrintStream ps)方式重新指定输入和输出的流。

打印流

PrintStream 和PrintWriter
提供了一系列重载的print()和println()

数据流

DataInputStream 和 DataOutputStream
作用:用于读取或写出基本数据类型的变量或字符串

对象流

1.ObjectInputStream 和 ObjectOutputStream

2.作用:

用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
序列化:用ObjectOutputStream类保存基本类型数据或对象的机制
反序列化:用ObjectInputStream类读取基本类型数据或对象的机制

ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

3.要想一个java对象是可序列化的,需要满足相应的要求。

(1)需要实现接口:Serializable
(2)当前类提供一个全局常量:serialVersionUID
(3)除了当前Person类需要实现Serializable接口之外,还必须保证其内部所有属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)
补充:ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

4.序列化机制:

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原
序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础
如果要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常。Serializable;Externalizable。

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:private static final long serialVersionUID;serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象
进行版本控制,有关各版本反序列化时是否兼容。如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议,显式声明。
简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

使用对象流序列化对象

若某个类实现了 Serializable 接口,该类的对象就是可序列化的:

  • 创建一个 ObjectOutputStream
  • 调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
  • 注意写出一次,操作flush()一次

反序列化

  • 创建一个 ObjectInputStream
  • 调用 readObject() 方法读取流中的对象

强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化

//序列化:将对象写入到磁盘或者进行网络传输。
//要求对象必须实现序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“data.txt"));
Person p = new Person("韩梅梅", 18, "中华大街", new Pet());
oos.writeObject(p);
oos.flush();
oos.close();

//反序列化:将磁盘中的对象数据源读出。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“data.txt"));
Person p1 = (Person)ois.readObject();
System.out.println(p1.toString());
ois.close();

@面:什么是java序列化,如何实现java序列化

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间,序列化是为了解决在对对象流进行读写操作时所引发的问题。

序列化的实现:
将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的。
然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象。
接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。

随机存取文件流

RandomAccessFile的使用

1.RandomAccessFile直接继承于java.lang.Object类,实现了DataInput和DataOutput接口,也就意味着这个类既可以读也可以写。

2.RandomAccessFile既可以作为一个输入流,又可以作为一个输出流
(1)如果RandomAccessFile作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。
(2)如果写出到的文件存在,则会对原有文件内容进行覆盖。(默认情况下,从头覆盖)

3.可以通过相关的操作,实现RandomAccessFile “插入”数据的效果

RandomAccessFile类

RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也可以写。
RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方来读、写文件

  • 支持只访问文件的部分内容
  • 可以向已存在的文件后追加内容

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。
RandomAccessFile 类对象可以自由移动记录指针:

  • long getFilePointer():获取文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到 pos 位置

构造器

public RandomAccessFile(File file, String mode) 
public RandomAccessFile(String name, String mode) 

创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:

r: 以只读方式打开
rw:打开以便读取和写入
rwd:打开以便读取和写入;同步文件内容的更新
rws:打开以便读取和写入;同步文件内容和元数据的更新

如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不存在则会去创建文件,如果存在则不会创建。

读取文件内容

RandomAccessFile raf = new RandomAccessFile(“test.txt”, “rw”);
raf.seek(5);
byte [] b = new byte[1024];
int off = 0;
int len = 5;
raf.read(b, off, len);
String str = new String(b, 0, len);
System.out.println(str);
raf.close();

写入文件内容

RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
raf.seek(5);
//先读出来
String temp = raf.readLine();
raf.seek(5);
raf.write("xyz".getBytes());
raf.write(temp.getBytes());
raf.close();

NIO.2

网络编程

目的:

直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。

两个问题

1.如何准确地定位网络上一台或多台主机;定位主机上的特定的应用。
2.找到主机后如何可靠高效地进行数据传输

两个要素

1.对应问题一:IP和端口号
2.对应问题二:提供网络通信协议:TCP/IP参考模型(应用层、传输层、网络层、物理、数据链路层)

IP和端口号

1.IP:唯一的标识Internet上的计算机(通信实体)
2.在Java中**使用InetAddress类代表IP**
3.IP分类:IPv4 和 IPv6 ; 万维网和局域网
4.域名:   www.baidu.com   www.mi.com  www.sina.com  
5.本地回路地址:127.0.0.1 对应着:localhost
6.如何**实例化InetAddress:**两个方法:getByName(String host) 、 getLocalHost()
7.两个常用方法:getHostName() / getHostAddress()
8.端口号:正在计算机上运行的进程。
要求:不同的进程有不同的端口号
范围:被规定为一个 16 位的整数 0~65535。
9.端口号与IP地址的组合得出一个网络套接字:Socket

先找本机hosts,是否有输入的域名地址,没有的话,再通过DNS服务器,找主机。

InetAdress类

InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取
InetAddress实例

public static InetAddress getLocalHost()
public static InetAddress getByName(String host)

InetAddress提供了如下几个常用的方法

public String getHostAddress():返回 IP 地址字符串(以文本表现形式)。 
public String getHostName():获取此 IP 地址的主机名
public boolean isReachable(int timeout):测试是否可以达到该地址

网络协议

网络通信协议
计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。

问题:网络协议太复杂
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?

通信协议分层的思想
在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不***。各层互不影响,利于系统的开发和扩展。

1.传输层协议中有两个非常重要的协议:

传输控制协议TCP(Transmission Control Protocol)
用户数据报协议UDP(User Datagram Protocol)。

2.TCP协议:

(1)使用TCP协议前,须先建立TCP连接,形成传输数据通道 
(2)传输前,采用“三次握手”方式,点对点通信,是可靠的 
(3)TCP协议进行通信的两个应用进程:客户端、服务端。 
(4)在连接中可进行大数据量的传输 
(5)传输完毕,需释放已建立的连接,效率低 

3.UDP协议:

(1)将数据、源、目的封装成数据包,不需要建立连接 
(2)每个数据报的大小限制在64K内 
(3)发送不管对方是否准备好,接收方收到也不确认,故是不可靠的 
(4)可以广播发送 
(5)发送数据结束时
img

@面:三次握手

三次握手的过程
1)主机A向主机B发送TCP连接请求数据包,其中包含主机A的初始序列号seq(A)=x。(其中报文中同步标志位SYN=1,确认号ACK=0,表示这是一个TCP连接请求数据报文;序号seq=x,表明传输数据时的第一个数据字节的序号是x);
2)主机B收到请求后,会发回连接确认数据包。(其中确认报文段中,标识位SYN=1,ACK=1,表示这是一个TCP连接响应数据报文,并含主机B的初始序列号seq(B)=y,以及主机B对主机A初始序列号的确认号ack(B)=seq(A)+1=x+1)
3)第三次,主机A收到主机B的确认报文后,还需作出确认,即发送一个序列号seq(A)=x+1;确认号为ack(A)=y+1的报文;
img

@面:TCP协议的4次握手

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

TCP的连接的拆除需要发送四个包,因此称为四次挥手
四次挥手过程
假设主机A为客户端,主机B为服务器,其释放TCP连接的过程如下:
1)关闭客户端到服务器的连接:首先客户端A发送一个FIN,用来关闭客户到服务器的数据传送,然后等待服务器的确认。其中终止标志位FIN=1,序列号seq=x。
2)服务器收到这个FIN,它发回一个ACK,确认号ack为收到的序号加1。
3)关闭服务器到客户端的连接:也是发送一个FIN给客户端。
4)客户段收到FIN后,并发回一个ACK报文确认,并将确认序号seq设置为收到序号加1。 首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

Socket

利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。

网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。

通信的两端都要有Socket,是两台机器间通信的端点。

网络通信其实就是Socket间的通信。

Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。

一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。

Socket分类:

  • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
  • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
Socket类的常用构造器:
public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 
public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号。  

Socket类的常用方法:
public InputStream getInputStream()返回此套接字的输入流。可以用于接收网络消息
public OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
public InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。 
public InetAddress getLocalAddress()获取套接字绑定的本地地址。 即本端的IP地址
public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。 
public int getLocalPort()返回此套接字绑定到的本地端口。 如果尚未绑定套接字,则返回 -1。即本端的端口号。
public void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和OutputStream。
public void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
public void shutdownOutput()禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。

TCP网络编程

image-20211008163137057

客户端Socket的工作过程包含以下四个基本的步骤:

  • 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
  • 打开连接到 Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
  • 按照一定的协议对 Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
  • 关闭 Socket:断开客户端到服务器的连接,释放线路
Socket s = new 
Socket(“192.168.40.165”,9999);
OutputStream out = s.getOutputStream();
out.write(" hello".getBytes());
s.close();

客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连接。Socket的构造器是:

Socket(String host,int port)throws UnknownHostException,IOException:向服务器(域名是host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。

Socket(InetAddress address,int port)throws IOException:根据InetAddress对象所表示的IP地址以及端口号port发起连接。

客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求

img

服务器程序的工作过程包含以下四个基本的步骤:

  • 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
  • 调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
  • 调用 该Socket类对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收。
  • 关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字。
ServerSocket ss = new ServerSocket(9999);
Socket s = ss.accept ();
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int num = in.read(buf);
String str = new String(buf,0,num);
System.out.println(s.getInetAddress().toString()+”:”+str);
s.close();
ss.close();

ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口中的业务员。也就是说,服务器必须事先建立一个等待客户请求建立套接字连接的ServerSocket对象。
所谓“接收”客户的套接字请求,就是accept()方***返回一个 Socket 对象

img

image-20211008163531529

UDP网络编程

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。

DatagramSocket类的常用方法

public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。 
public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。 
public void close()关闭此数据报套接字。 
public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。 
public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
public InetAddress getLocalAddress()获取套接字绑定的本地地址。
public int getLocalPort()返回此套接字绑定的本地主机上的端口号。 
public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回 null。 
public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。

DatagramPacket类的常用方法

public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。 
public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length参数必须小于等于 buf.length。 
public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。 
public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。 
public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。
public int getLength()返回将要发送或接收到的数据的长度。

流 程:

  1. DatagramSocket与DatagramPacket
  2. 建立发送端,接收端
  3. 建立数据包
  4. 调用Socket的发送、接收方法
  5. 关闭Socket

发送端与接收端是两个独立的运行程序

发送端

DatagramSocket ds = null;
try {
   
    ds = new DatagramSocket();
    byte[] by = "hello,atguigu.com".getBytes();
    DatagramPacket dp = new DatagramPacket(by, 0, by.length, 
                                           InetAddress.getByName("127.0.0.1"), 10000);
    ds.send(dp);
} catch (Exception e) {
   
    e.printStackTrace();
} finally {
   
    if (ds != null)
        ds.close();
}

接收端

DatagramSocket ds = null;
try {
   
    ds = new DatagramSocket(10000);
    byte[] by = new byte[1024];
    DatagramPacket dp = new DatagramPacket(by, by.length);
    ds.receive(dp);
    String str = new String(dp.getData(), 0, dp.getLength());
    System.out.println(str + "--" + dp.getAddress());
} catch (Exception e) {
   
    e.printStackTrace();
} finally {
   
    if (ds != null)
        ds.close();
}
img

URL编程

img

URL类

URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。

它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。

URL的基本结构由5部分组成: <传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表

  • 例如: http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
  • #片段名:即锚点,例如看小说,直接定位到章节
  • 参数列表格式:参数名=参数值&参数名=参数值…

URL类构造器

为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象:

public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。
例如:URL url = new URL ("http://www. atguigu.com/"); 

public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。
例如:URL downloadUrl = new URL(url, “download.html")

public URL(String protocol, String host, String file); 
例如:new URL("http", "www.atguigu.com", “download. html");

public URL(String protocol, String host, int port, String file); 
例如: URL gamelan = new URL("http", "www.atguigu.com", 80, “download.html");

URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。

URL类常用方法

一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:
public String getProtocol( ) 获取该URL的协议名
public String getHost( ) 获取该URL的主机名
public String getPort( ) 获取该URL的端口号
public String getPath( ) 获取该URL的文件路径
public String getFile( ) 获取该URL的文件名
public String getQuery( ) 获取该URL的查询名

针对HTTP协议的URLConnection类

URL的方法 openStream():能从网络上读取数据
若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用URLConnection 。

URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection对象。如果连接过程失败,将产生IOException.

  • URL netchinaren = new URL (“http://www.atguigu.com/index.shtml”);
  • URLConnectonn u = netchinaren.openConnection( );

通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。

public Object getContent( ) throws IOException
public int getContentLength( )
public String getContentType( )
public long getDate( )
public long getLastModified( )
public InputStream getInputStream( )throws IOException
public OutputSteram getOutputStream( )throws IOException

URI、URL和URN的区别

URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。而URL是uniform resource locator,统一资源定位符,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。而URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI。 在Java的URI中,一个URI实例可以代表绝对的,也可以是相对的,只要它符合URI的语法规则。而URL类则不仅符合语义,还包含了定位该资源的信息,因此它不能是相对的

image-20211008165311537

反射机制

反射机制概述

1)什么是反射

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

image-20210319203356101

2)反射相关的主要API

image-20210319204620268

@面:反射的实现过程和作用

JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。

反射的实现主要借助以下四个类:
Class:类的对象,Constructor:类的构造方法,Field:类中的属性对象,Method:类中的方法对象。

作用:反射机制指的是程序在运行时能够获取自身的信息。在JAVA中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。

关于Class类的理解

Class类即为class字节码文件?

在Object类中定义了以下的方法,此方法将被所有子类继承:public final Class getClass()

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

1.类的加载过程:

程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class的一个实例

2.换句话说,Class的实例就对应着一个运行时类

3.加载到内存中的运行时类,会缓存一定的时间,在此时间之内,我们可以通过不同的方式来获取此运行时类。

4.获取Class实例方法

1)前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
实例:Class clazz = String.class; 

2)前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
实例:Class clazz = “www.atguigu.com”.getClass();

3)前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
实例:Class clazz = Class.forName(“java.lang.String”);

4)其他方式(不做要求)
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass(“类的全类名”);
img

5.Class类的常用方法

static Class forName(String name) 
    返回指定类名name的Class对象
Object newInstance() 
    调用缺省构造函数,返回该Class对象的一个实例
getName() 
    返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass() 
    返回当前Class对象的父类的Class对象
Class [] getInterfaces() 
    获取当前Class对象的接口
ClassLoader getClassLoader() 
    返回该类的类加载器
Class getSuperclass() 
    返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 
    返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 
    返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes)
    返回一个Method对象,此对象的形参类型为paramType

哪些类型可以有Class对象

(1)class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void

Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11);

类的加载

类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

image-20211008171339529

加载

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。

链接

将Java类的二进制代码合并到JVM的运行状态之中的过程。

  • 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
  • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

初始化

  • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

类加载器的作用:

  • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
  • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

创建运行时类的对象

Class<Person> clazz = Person.class;
Person obj = clazz.newInstance();

newInstance():

调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。要想此方法正常的创建运行时类的对象,
要求:
1.运行时类必须提供空参的构造器
2.空参的构造器的访问权限得够。通常设置为public。
难道没有无参的构造器就不能创建对象了吗?
不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
步骤如下:
1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。
//1.根据全类名获取对应的Class对象
String name =atguigu.java.Person";
Class clazz = null;
clazz = Class.forName(name);
//2.调用指定参数结构的构造器,生成Constructor的实例
Constructor con = clazz.getConstructor(String.class,Integer.class);
//3.通过Constructor的实例创建对应类的对象,并初始化类属性
Person p2 = (Person) con.newInstance("Peter",20);
System.out.println(p2);

@面:如何通过反射创建对象

方法1:通过类对象调用newInstance()方法,例如:
    String.class.newInstance()
        
方法2:通过类对象的getConstructor()getDeclaredConstructor()方法获得构造器(Constructor)对象,并调用其newInstance()方法创建对象,例如:
    String.class.getConstructor(String.class).newInstance("Hello");

获取运行时类的完整结构

1)获取属性:

public Field[] getFields() 
返回此Class对象所表示的类或接口的publicFieldpublic Field[] getDeclaredFields() 
返回此Class对象所表示的类或接口的全部FieldField方法中:即属性中的信息,如 private String name;
public int getModifiers() 以整数形式返回此Field的修饰符
public Class<?> getType() 得到Field的属性类型
public String getName() 返回Field的名称。

2)获取构造器:

public Constructor<T>[] getConstructors()
返回此 Class 对象所表示的类的所有public构造方法。
public Constructor<T>[] getDeclaredConstructors()
返回此 Class 对象表示的类声明的所有构造方法。
    
Constructor类中:如 Person(String name,int age){
   }
取得修饰符: public int getModifiers();
取得方法名称: public String getName();
取得参数的类型:public Class<?>[] getParameterTypes();

3)获取方法:

public Method[] getDeclaredMethods()
返回此Class对象所表示的类或接口的全部方法
public Method[] getMethods() 
返回此Class对象所表示的类或接口的public的方法
    
Method类中:如 
private String show(String nation) throws XxException {
   }

public Class<?> getReturnType()取得全部的返回值
public Class<?>[] getParameterTypes()取得全部的参数
public int getModifiers()取得修饰符
public Class<?>[] getExceptionTypes()取得异常信息

4)获取其他:

实现的全部接口
public Class<?>[] getInterfaces() 
确定此对象所表示的类或接口实现的接口。
    
所继承的父类
public Class<? Super T> getSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的ClassAnnotation相关
get Annotation(Class<T> annotationClass) 
getDeclaredAnnotations() 
    
泛型相关
获取父类泛型类型:Type getGenericSuperclass()
泛型类型:ParameterizedType
获取实际的泛型类型参数数组:getActualTypeArguments()
    
类所在的包 Package getPackage()

调用运行时类的指定结构

1)调用指定属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。

public Field getField(String name) 
	返回此Class对象表示的类或接口的指定的publicFieldpublic Field getDeclaredField(String name)
    返回此Class对象表示的类或接口的指定的Field。
    
在Field中:
public Object get(Object obj) 
    取得指定对象obj上此Field的属性内容
public void set(Object obj,Object value) 
    设置指定对象obj上此Field的属性内容

setAccessible方法

Method和Field、Constructor对象都有setAccessible()方法。
setAccessible启动和禁用访问安全检查的开关。

参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。 
	提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。 
	使得原本无法访问的私有成员也可以访问
参数值为false则指示反射的对象应该实施Java语言访问检查。

2)调用指定方法

步骤:

1.通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
2.之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象的参数信息。
image-20210321115509245
Object invoke(Object obj, Object … args)
说明:
1.Object 对应原方法的返回值,若原方法无返回值,此时返回null
2.若原方法若为静态方法,此时形参Object obj可为null
3.若原方法形参列表为空,则Object[] args为null
4.若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法,将可访问private的方法。

@面:如何通过反射获取和设置对象私有字段的值

可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
    
下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以获得dog对象的主人的汽车的引擎的ID号。
    
import java.lang.reflect.Method;
class MethodInvokeTest {
   
    public static void main(String[] args) throws Exception {
   
        String str = "hello";
    Method m = str.getClass().getMethod("toUpperCase");
        System.out.println(m.invoke(str));  // HELLO
    }
}

反射的应用-动态代理

1、相关概念

代理设计模式的原理: 
使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

之前为大家讲解过代理机制的操作,属于静态代理,特征是代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。最好可以通过一个代理类完成全部的代理功能(即其他代理类要实现的功能基本一致,可只需设计一个代理类,在运行期间再看加载的具体是哪个代理类)。

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

动态代理使用场合:
    调试
    远程方法调用

动态代理相比于静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中
处理,这样,我们可以更加灵活和统一的处理众多的方法。

静态代理举例

/**
 * 静态代理举例
 *
 * 特点:代理类和被代理类在编译期间,就确定下来了。
 *
 * @author shkstart
 * @create 2019 上午 10:11
 */
interface ClothFactory{

    void produceCloth();
}

//代理类
class ProxyClothFactory implements ClothFactory{
    private ClothFactory factory;//用被代理类对象进行实例化

    public ProxyClothFactory(ClothFactory factory){
        this.factory = factory;
    }

    @Override
    public void produceCloth() {
        System.out.println("代理工厂做一些准备工作");

        factory.produceCloth();

        System.out.println("代理工厂做一些后续的收尾工作");
    }
}

//被代理类
class NikeClothFactory implements ClothFactory{

    @Override
    public void produceCloth() {
        System.out.println("Nike工厂生产一批运动服");
    }
}

public class StaticProxyTest {
    public static void main(String[] args) {
        //创建被代理类的对象
        ClothFactory nike = new NikeClothFactory();
        //创建代理类的对象
        ClothFactory proxyClothFactory = new ProxyClothFactory(nike);

        proxyClothFactory.produceCloth();
    }
}
image-20210718131406064

动态代理的举例

/** * 动态代理的举例 */

interface Human{
   
    String getBelief();
    void eat(String food);
}

//被代理类
class SuperMan implements Human{
   
    @Override
    public String getBelief() {
   
        return "I believe I can fly!";
    }

    @Override
    public void eat(String food) {
   
        System.out.println("我喜欢吃" + food);
    }
}

/* 要想实现动态代理,需要解决的问题? 问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。 问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。 */
class ProxyFactory{
   

    //obj:被代理类的对象
    public static Object getProxyInstance(Object obj){
   
    
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);
        
		//返回一个代理类的对象,解决问题一
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
        	obj.getClass().getInterfaces(),handler);
    }
}

class MyInvocationHandler implements InvocationHandler{
   
    private Object obj;//需要使用被代理类的对象进行赋值

    public void bind(Object obj){
   
        this.obj = obj;
    }

    //当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    //将被代理类要执行的方法a的功能就声明在invoke()中
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   
    
        //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
        //obj:被代理类的对象
        Object returnValue = method.invoke(obj,args);

        //上述方法的返回值就作为当前类中的invoke()的返回值。
        return returnValue;
    }
}

public class ProxyTest {
   

    public static void main(String[] args) {
   
    	//被代理类对象
        SuperMan superMan = new SuperMan();
        
        //代理类的对象
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        
        //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        String belief = proxyInstance.getBelief();
        System.out.println(belief);
        
        proxyInstance.eat("四川麻辣烫");
    }
}
2、相关API
Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。

提供用于创建动态代理类和动态代理对象的静态方法
1)创建一个动态代理类所对应的Class对象
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 
2)直接创建一个动态代理对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
参数:
类加载器 
得到被代理类实现的全部接口
得到InvocationHandler接口的实现类实例
3、动态代理步骤
1)创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。

//参数
代理类的对象
要调用的方法
方法调用时所需要的参数
public Object invoke(Object theProxy, Method method, Object[] params) throws Throwable{
    try{
        Object retval = method.invoke(targetObj, params);
        // Print out the result
        System.out.println(retval);
        return retval;
    } catch (Exception exc){}
}
2)创建被代理的类以及接口
image-20210718134421905
3)通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理
RealSubject target = new RealSubject();
// Create a proxy to wrap the original implementation
DebugProxy proxy = new DebugProxy(target);
// Get a reference to the proxy through the Subject interface
Subject sub = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),new Class[] { Subject.class }, proxy);
4)通过 Subject代理调用RealSubject实现类的方法
String info = sub.say(“Peter", 24);
System.out.println(info)

Java8新特性

image-20210718142308300

Lambda表达式

从匿名类到 Lambda 的转换举例1
image-20210718191212703
从匿名类到 Lambda 的转换举例2
image-20210718191233133
语法:
image-20210718191329673 image-20210718191346281

@面:Lamda表达式的优缺点

优点:1. 简洁。2. 非常容易并行计算。3. 可能代表未来的编程趋势。
缺点:1. 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)2. 不容易调试。3. 若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂。

函数式接口

1、什么是函数式(Functional)接口

只包含一个抽象方法的接口,称为函数式接口。
你可以通过 Lambda 表达式来创建该接口的对象。(若Lambda表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
在java.util.function包下定义了Java 8的丰富的函数式接口

2、如何理解函数式接口

Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还可以支持OOF(面向函数编程)
在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。
简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
所以以前用匿名实现类表示的现在都可以用Lambda表达式来写

3、函数式接口举例
image-20210718202427591
4、自定义函数式接口
image-20210718202504279
5、作为参数传递Lambda表达式
为了将Lambda表达式作为参数传递,接收Lambda表达式的参数类型必须是与该Lambda表达式兼容的函数式接口的类型。
image-20210718202805739
6、Java内置四大核心函数式接口
image-20210718202850787
/**
 * java内置的4大核心函数式接口
 *
 * 消费型接口 Consumer<T>     void accept(T t)
 * 供给型接口 Supplier<T>     T get()
 * 函数型接口 Function<T,R>   R apply(T t)
 * 断定型接口 Predicate<T>    boolean test(T t)
 *
 */
public class LambdaTest2 {

    @Test
    public void test1(){

        happyTime(500, new Consumer<Double>() {
            @Override
            public void accept(Double aDouble) {
                System.out.println("学习太累了,去天上人间买了瓶矿泉水,价格为:" + aDouble);
            }
        });

        System.out.println("********************");

        happyTime(400,money -> System.out.println("学习太累了,去天上人间喝了口水,价格为:" + money));
    }

    public void happyTime(double money, Consumer<Double> con){
        con.accept(money);
    }

    @Test
    public void test2(){
        List<String> list = Arrays.asList("北京","南京","天津","东京","西京","普京");

        List<String> filterStrs = filterString(list, new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.contains("京");
            }
        });
        System.out.println(filterStrs);

        List<String> filterStrs1 = filterString(list,s -> s.contains("京"));
        System.out.println(filterStrs1);
    }

    //根据给定的规则,过滤集合中的字符串。此规则由Predicate的方法决定
    public List<String> filterString(List<String> list, Predicate<String> pre){

        ArrayList<String> filterList = new ArrayList<>();

        for(String s : list){
            if(pre.test(s)){
                filterList.add(s);
            }
        }
        return filterList;
    }
}
7、其他接口
image-20210718203218670

方法引用与构造器引用

1、方法引用

当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。

要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!

格式:使用操作符 “::” 将类(或对象) 与方法名分隔开来。

如下三种主要使用情况:
对象::实例方法名
类::静态方法名
类::实例方法名
image-20210718214427304 image-20210718214443456

注意:当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName

public class Employee {
   
	private int id;
	private String name;
	private int age;
	private double salary;
}
/** * 提供用于测试的数据 * */
public class EmployeeData {
   
	
	public static List<Employee> getEmployees(){
   
		List<Employee> list = new ArrayList<>();
		
		list.add(new Employee(1001, "马化腾", 34, 6000.38));
		list.add(new Employee(1002, "马云", 12, 9876.12));
		list.add(new Employee(1003, "刘强东", 33, 3000.82));
		list.add(new Employee(1004, "雷军", 26, 7657.37));
		list.add(new Employee(1005, "李彦宏", 65, 5555.32));
		list.add(new Employee(1006, "比尔盖茨", 42, 9500.43));
		list.add(new Employee(1007, "任正非", 26, 4333.32));
		list.add(new Employee(1008, "扎克伯格", 35, 2500.32));
	
		return list;
	}	
}
/**
 * 方法引用的使用
 *
 * 1.使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
 *
 * 2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例。
 *   所以方法引用,也是函数式接口的实例。
 *
 * 3. 使用格式:  类(或对象) :: 方法名
 *
 * 4. 具体分为如下的三种情况:
 *    情况1     对象 :: 非静态方法
 *    情况2     类 :: 静态方法
 *    情况3     类 :: 非静态方法
 *
 * 5. 方法引用使用的要求:要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的
 *    形参列表和返回值类型相同!(针对于情况1和情况2)
 */
public class MethodRefTest {

	// 情况一:对象 :: 实例方法
	//Consumer中的void accept(T t)    该方法为接口中的抽象方法
	//PrintStream中的void println(T t)   该方法为方法引用的方法
	@Test
	public void test1() {
		Consumer<String> con1 = str -> System.out.println(str);
		con1.accept("北京");

		System.out.println("*******************");
		PrintStream ps = System.out;
		Consumer<String> con2 = ps::println;
		con2.accept("beijing");
	}
	
	//Supplier中的T get()
	//Employee中的String getName()
	@Test
	public void test2() {
		Employee emp = new Employee(1001,"Tom",23,5600);

		Supplier<String> sup1 = () -> emp.getName();
		System.out.println(sup1.get());

		System.out.println("*******************");
		Supplier<String> sup2 = emp::getName;
		System.out.println(sup2.get());

	}

	// 情况二:类 :: 静态方法
	//Comparator中的int compare(T t1,T t2)
	//Integer中的int compare(T t1,T t2)
	@Test
	public void test3() {
		Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
		System.out.println(com1.compare(12,21));

		System.out.println("*******************");

		Comparator<Integer> com2 = Integer::compare;
		System.out.println(com2.compare(12,3));

	}
	
	//Function中的R apply(T t)
	//Math中的Long round(Double d)
	@Test
	public void test4() {
		Function<Double,Long> func = new Function<Double, Long>() {
			@Override
			public Long apply(Double d) {
				return Math.round(d);
			}
		};

		System.out.println("*******************");

		Function<Double,Long> func1 = d -> Math.round(d);
		System.out.println(func1.apply(12.3));

		System.out.println("*******************");

		Function<Double,Long> func2 = Math::round;
		System.out.println(func2.apply(12.6));
	}

	// 情况三:类 :: 实例方法  (有难度)
	// Comparator中的int comapre(T t1,T t2)
	// String中的int t1.compareTo(t2)
	@Test
	public void test5() {
		Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
		System.out.println(com1.compare("abc","abd"));

		System.out.println("*******************");

		Comparator<String> com2 = String :: compareTo;
		System.out.println(com2.compare("abd","abm"));
	}

	//BiPredicate中的boolean test(T t1, T t2);
	//String中的boolean t1.equals(t2)
	@Test
	public void test6() {
		BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
		System.out.println(pre1.test("abc","abc"));

		System.out.println("*******************");
		BiPredicate<String,String> pre2 = String :: equals;
		System.out.println(pre2.test("abc","abd"));
	}
	
	// Function中的R apply(T t)
	// Employee中的String getName();
	@Test
	public void test7() {
		Employee employee = new Employee(1001, "Jerry", 23, 6000);

		Function<Employee,String> func1 = e -> e.getName();
		System.out.println(func1.apply(employee));

		System.out.println("*******************");

		Function<Employee,String> func2 = Employee::getName;
		System.out.println(func2.apply(employee));

	}
}

2、构造器引用

格式: ClassName::new 
与函数式接口相结合,自动与函数式接口中方法兼容。
可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
image-20210718215524278
数组引用

格式: type[] :: new
image-20210718215632976
/**
 * 一、构造器引用
 *      和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。
 *      抽象方法的返回值类型即为构造器所属的类的类型
 *
 * 二、数组引用
 *     大家可以把数组看做是一个特殊的类,则写法与构造器引用一致。
 */
public class ConstructorRefTest {
	//构造器引用
    //Supplier中的T get()
    //Employee的空参构造器:Employee()
    @Test
    public void test1(){

        Supplier<Employee> sup = new Supplier<Employee>() {
            @Override
            public Employee get() {
                return new Employee();
            }
        };
        System.out.println("*******************");

        Supplier<Employee>  sup1 = () -> new Employee();
        System.out.println(sup1.get());

        System.out.println("*******************");

        Supplier<Employee>  sup2 = Employee :: new;
        System.out.println(sup2.get());
    }

	//Function中的R apply(T t)
    @Test
    public void test2(){
        Function<Integer,Employee> func1 = id -> new Employee(id);
        Employee employee = func1.apply(1001);
        System.out.println(employee);

        System.out.println("*******************");

        Function<Integer,Employee> func2 = Employee :: new;
        Employee employee1 = func2.apply(1002);
        System.out.println(employee1);

    }

	//BiFunction中的R apply(T t,U u)
    @Test
    public void test3(){
        BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
        System.out.println(func1.apply(1001,"Tom"));

        System.out.println("*******************");

        BiFunction<Integer,String,Employee> func2 = Employee :: new;
        System.out.println(func2.apply(1002,"Tom"));

    }

	//数组引用
    //Function中的R apply(T t)
    @Test
    public void test4(){
        Function<Integer,String[]> func1 = length -> new String[length];
        String[] arr1 = func1.apply(5);
        System.out.println(Arrays.toString(arr1));

        System.out.println("*******************");

        Function<Integer,String[]> func2 = String[] :: new;
        String[] arr2 = func2.apply(10);
        System.out.println(Arrays.toString(arr2));
    }
}

Stream API

1、Stream API说明

Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。

Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

2、为什么要使用Stream API

实际开发中,项目中多数数据源都来自于Mysql,Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

3、什么是 Stream

Stream到底是什么呢?

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,Stream讲的是计算!”

注意:

①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

4、Stream 的操作三个步骤

1)创建 Stream
一个数据源(如:集合、数组),获取一个流
2)中间操作
一个中间操作链,对数据源的数据进行处理
3)终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

image-20210719113219854

5、创建Stream方式

5.1 通过集合

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法: 
default Stream<E> stream() : 返回一个顺序流
default Stream<E> parallelStream() : 返回一个并行流
5.2 通过数组

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
static <T> Stream<T> stream(T[] array): 返回一个流

重载形式,能够处理对应基本类型的数组:
public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)
5.3 通过Stream的of()

可以调用Stream类静态方法of(), 通过显示值创建一个流。它可以接收任意数量的参数。
public static<T> Stream<T> of(T... values) : 返回一个流
5.4 创建无限流

可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。
迭代
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) 
生成
public static<T> Stream<T> generate(Supplier<T> s)
/**
 * 1. Stream关注的是对数据的运算,与CPU打交道
 *    集合关注的是数据的存储,与内存打交道
 *
 * 2.
 * ①Stream 自己不会存储元素。
 * ②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
 * ③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
 *
 * 3.Stream 执行流程
 * ① Stream的实例化
 * ② 一系列的中间操作(过滤、映射、...)
 * ③ 终止操作
 *
 * 4.说明:
 * 4.1 一个中间操作链,对数据源的数据进行处理
 * 4.2 一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
 *
 *
 *  测试Stream的实例化
 */
public class StreamAPITest {

    //创建 Stream方式一:通过集合
    @Test
    public void test1(){
        List<Employee> employees = EmployeeData.getEmployees();

//        default Stream<E> stream() : 返回一个顺序流
        Stream<Employee> stream = employees.stream();

//        default Stream<E> parallelStream() : 返回一个并行流
        Stream<Employee> parallelStream = employees.parallelStream();
    }

    //创建 Stream方式二:通过数组
    @Test
    public void test2(){
        int[] arr = new int[]{1,2,3,4,5,6};
        //调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
        IntStream stream = Arrays.stream(arr);

        Employee e1 = new Employee(1001,"Tom");
        Employee e2 = new Employee(1002,"Jerry");
        Employee[] arr1 = new Employee[]{e1,e2};
        Stream<Employee> stream1 = Arrays.stream(arr1);

    }
    //创建 Stream方式三:通过Stream的of()
    @Test
    public void test3(){
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
    }

    //创建 Stream方式四:创建无限流
    @Test
    public void test4(){

//      迭代
//      public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
        //遍历前10个偶数
        Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

//      生成
//      public static<T> Stream<T> generate(Supplier<T> s)
        Stream.generate(Math::random).limit(10).forEach(System.out::println);
    }
}
6、Stream 的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
image-20210719115531154 image-20210719115552726 image-20210719115616592
/** * 测试Stream的中间操作 */
public class StreamAPITest1 {
   

    //1-筛选与切片
    @Test
    public void test1(){
   
        List<Employee> list = EmployeeData.getEmployees();
		//filter(Predicate p)——接收 Lambda , 从流中排除某些元素。
        Stream<Employee> stream = list.stream();
        //练习:查询员工表中薪资大于7000的员工信息
        stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);

        System.out.println();
		//limit(n)——截断流,使其元素不超过给定数量。
        list.stream().limit(3).forEach(System.out::println);
        System.out.println();

		//skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
        list.stream().skip(3).forEach(System.out::println);
        System.out.println();
        
		//distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
        list.add(new Employee(1010,"刘强东",40,8000));
        list.add(new Employee(1010,"刘强东",41,8000));
        list.add(new Employee(1010,"刘强东",40,8000));
        list.add(new Employee(1010,"刘强东",40,8000));
        list.add(new Employee(1010,"刘强东",40,8000));

		//System.out.println(list);
        list.stream().distinct().forEach(System.out::println);
    }

    //映射
    @Test
    public void test2(){
   
		//map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
        List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
        list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

		//练习1:获取员工姓名长度大于3的员工的姓名。
        List<Employee> employees = EmployeeData.getEmployees();
        Stream<String> namesStream = employees.stream().map(Employee::getName);
        namesStream.filter(name -> name.length() > 3).forEach(System.out::println);
        System.out.println();
        
        //练习2:
        Stream<Stream<Character>> streamStream = 
            list.stream().map(StreamAPITest1::fromStringToStream);
        streamStream.forEach(s ->{
   
            s.forEach(System.out::println);
        });
        System.out.println();
        
		//flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
        Stream<Character> characterStream = 
            list.stream().flatMap(StreamAPITest1::fromStringToStream);
        characterStream.forEach(System.out::println);
    }

    //将字符串中的多个字符构成的集合转换为对应的Stream的实例
    public static Stream<Character> fromStringToStream(String str){
   //aa
        ArrayList<Character> list = new ArrayList<>();
        for(Character c : str.toCharArray()){
   
            list.add(c);
        }
       return list.stream();
    }

    @Test
    public void test3(){
   
        ArrayList list1 = new ArrayList();
        list1.add(1);
        list1.add(2);
        list1.add(3);

        ArrayList list2 = new ArrayList();
        list2.add(4);
        list2.add(5);
        list2.add(6);

// list1.add(list2);
        list1.addAll(list2);
        System.out.println(list1);
    }

    //3-排序
    @Test
    public void test4(){
   
		//sorted()——自然排序
        List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);
        list.stream().sorted().forEach(System.out::println);
        //抛异常,原因:Employee没有实现Comparable接口
		//List<Employee> employees = EmployeeData.getEmployees();
		//employees.stream().sorted().forEach(System.out::println);

		//sorted(Comparator com)——定制排序
        List<Employee> employees = EmployeeData.getEmployees();
        employees.stream().sorted( (e1,e2) -> {
   
           int ageValue = Integer.compare(e1.getAge(),e2.getAge());
           if(ageValue != 0){
   
               return ageValue;
           }else{
   
               return -Double.compare(e1.getSalary(),e2.getSalary());
           }
        }).forEach(System.out::println);
    }
}

7、Stream 的终止操作

端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
流进行了终止操作后,不能再次使用。

image-20210719115832110 image-20210719115846021 image-20210719120037290 1647252022131

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。
另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

/** * 测试Stream的终止操作 * * @author shkstart * @create 2019 下午 6:37 */
public class StreamAPITest2 {
   

    //1-匹配与查找
    @Test
    public void test1(){
   
        List<Employee> employees = EmployeeData.getEmployees();

// allMatch(Predicate p)——检查是否匹配所有元素。
// 练习:是否所有的员工的年龄都大于18
        boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
        System.out.println(allMatch);

// anyMatch(Predicate p)——检查是否至少匹配一个元素。
// 练习:是否存在员工的工资大于 10000
        boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 10000);
        System.out.println(anyMatch);

// noneMatch(Predicate p)——检查是否没有匹配的元素。
// 练习:是否存在员工姓“雷”
        boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷"));
        System.out.println(noneMatch);
// findFirst——返回第一个元素
        Optional<Employee> employee = employees.stream().findFirst();
        System.out.println(employee);
// findAny——返回当前流中的任意元素
        Optional<Employee> employee1 = employees.parallelStream().findAny();
        System.out.println(employee1);

    }

    @Test
    public void test2(){
   
        List<Employee> employees = EmployeeData.getEmployees();
        // count——返回流中元素的总个数
        long count = employees.stream().filter(e -> e.getSalary() > 5000).count();
        System.out.println(count);
// max(Comparator c)——返回流中最大值
// 练习:返回最高的工资:
        Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
        Optional<Double> maxSalary = salaryStream.max(Double::compare);
        System.out.println(maxSalary);
// min(Comparator c)——返回流中最小值
// 练习:返回最低工资的员工
        Optional<Employee> employee = employees.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
        System.out.println(employee);
        System.out.println();
// forEach(Consumer c)——内部迭代
        employees.stream().forEach(System.out::println);

        //使用集合的遍历操作
        employees.forEach(System.out::println);
    }

    //2-归约
    @Test
    public void test3(){
   
// reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
// 练习1:计算1-10的自然数的和
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        Integer sum = list.stream().reduce(0, Integer::sum);
        System.out.println(sum);


// reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
// 练习2:计算公司所有员工工资的总和
        List<Employee> employees = EmployeeData.getEmployees();
        Stream<Double> salaryStream = employees.stream().map(Employee::getSalary);
// Optional<Double> sumMoney = salaryStream.reduce(Double::sum);
        Optional<Double> sumMoney = salaryStream.reduce((d1,d2) -> d1 + d2);
        System.out.println(sumMoney.get());

    }

    //3-收集
    @Test
    public void test4(){
   
		//collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
		//练习1:查找工资大于6000的员工,结果返回为一个List或Set
        List<Employee> employees = EmployeeData.getEmployees();
        List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());

        employeeList.forEach(System.out::println);
        System.out.println();
        Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());

        employeeSet.forEach(System.out::println);
    }
}
8、强大的Stream API: Collectors
image-20210719120318099 image-20210719120343342

Optional类

到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。
Optional<T> 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。 
Optional类的Javadoc描述如下:这是一个可以为null的容器对象。如果值存在则isPresent()方***返回true,调用get()方***返回该对象。
Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
创建Optional类对象的方法:
    Optional.of(T t) : 创建一个 Optional 实例,t必须非空; 
    Optional.empty() : 创建一个空的 Optional 实例
    Optional.ofNullable(T t):t可以为null
    
判断Optional容器中是否包含对象:
    boolean isPresent() : 判断是否包含对象
    void ifPresent(Consumer<? super T> consumer) :如果有值,就执行Consumer接口的实现代码,并且该值会作为参数传给它。
    
获取Optional容器的对象:
    T get(): 如果调用对象包含值,返回该值,否则抛异常
    T orElse(T other) :如果有值则将其返回,否则返回指定的other对象。
    T orElseGet(Supplier<? extends T> other) :如果有值则将其返回,否则返回由Supplier接口实现提供的对象。
    T orElseThrow(Supplier<? extends X> exceptionSupplier) :如果有值则将其返回,否则抛出由Supplier接口实现提供的异常。
image-20210719120612872 image-20210719120629539
public class Boy {
    private Girl girl;

    @Override
    public String toString() {
        return "Boy{" +
                "girl=" + girl +
                '}';
    }

    public Girl getGirl() {
        return girl;
    }

    public void setGirl(Girl girl) {
        this.girl = girl;
    }

    public Boy() {

    }

    public Boy(Girl girl) {

        this.girl = girl;
    }
}
public class Girl {

    private String name;

    @Override
    public String toString() {
        return "Girl{" +
                "name='" + name + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Girl() {

    }

    public Girl(String name) {

        this.name = name;
    }
}
/**
 * Optional类:为了在程序中避免出现空指针异常而创建的。
 *
 * 常用的方法:ofNullable(T t)
 *            orElse(T t)
 *
 * @author shkstart
 * @create 2019 下午 7:24
 */
public class OptionalTest {

/*
Optional.of(T t) : 创建一个 Optional 实例,t必须非空;
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):t可以为null
 */
    @Test
    public void test1(){
        Girl girl = new Girl();
//        girl = null;
        //of(T t):保证t是非空的
        Optional<Girl> optionalGirl = Optional.of(girl);
    }

    @Test
    public void test2(){
        Girl girl = new Girl();
//        girl = null;
        //ofNullable(T t):t可以为null
        Optional<Girl> optionalGirl = Optional.ofNullable(girl);
        System.out.println(optionalGirl);
        //orElse(T t1):如果单前的Optional内部封装的t是非空的,则返回内部的t.
        //如果内部的t是空的,则返回orElse()方法中的参数t1.
        Girl girl1 = optionalGirl.orElse(new Girl("赵丽颖"));
        System.out.println(girl1);

    }


    public String getGirlName(Boy boy){
        return boy.getGirl().getName();
    }

    @Test
    public void test3(){
        Boy boy = new Boy();
        boy = null;
        String girlName = getGirlName(boy);
        System.out.println(girlName);

    }
    //优化以后的getGirlName():
    public String getGirlName1(Boy boy){
        if(boy != null){
            Girl girl = boy.getGirl();
            if(girl != null){
                return girl.getName();
            }
        }

        return null;
    }

    @Test
    public void test4(){
        Boy boy = new Boy();
        boy = null;
        String girlName = getGirlName1(boy);
        System.out.println(girlName);

    }

    //使用Optional类的getGirlName():
    public String getGirlName2(Boy boy){

        Optional<Boy> boyOptional = Optional.ofNullable(boy);
        //此时的boy1一定非空
        Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));

        Girl girl = boy1.getGirl();

        Optional<Girl> girlOptional = Optional.ofNullable(girl);
        //girl1一定非空
        Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));

        return girl1.getName();
    }

    @Test
    public void test5(){
        Boy boy = null;
        boy = new Boy();
        boy = new Boy(new Girl("苍老师"));
        String girlName = getGirlName2(boy);
        System.out.println(girlName);
    }
}

@面:解释一下extends 和super 泛型限定符

(1)泛型中上界和下界的定义
上界<? extend Fruit>
下界<? super Apple>

(2)上界和下界的特点
上界的list只能get,不能add(确切地说不能add出除null之外的对象,包括Object)
下界的list只能add,不能get

归根结底可以用一句话表示,那就是编译器可以支持向上转型,但不支持向下转型。具体来讲,我可以把Apple对象赋值给Fruit的引用,但是如果把Fruit对象赋值给Apple的引用就必须得用cast。

@面:请简单介绍一下java8的新特性

Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。

方法引用− 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

默认方法− 默认方法就是一个在接口里面有了一个实现的方法。

新工具− 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

Date Time API − 加强对日期与时间的处理。

Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。