# JavaSE-Learning
**Repository Path**: luo-chen508/java-se-learning
## Basic Information
- **Project Name**: JavaSE-Learning
- **Description**: javase学习一下
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-08-15
- **Last Updated**: 2025-08-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
#### 1 javaSE基础语法
##### 1、注释:3种 单行注释`//`、多行注释`/**/`、文档注释`/** * */`
##### 2、 字面量 注意空值是`null`
##### 3、变量
##### 4、变量里数据的存储原理:
- 二进制、8进制、十六进制、ASCLL码、`13 = 0B1101 = 015 = 0Xd`
##### 5、数据类型
- 8种基本数据类型:整型`byte short int(默认) long`、浮点型`float double(默认)`、字符型`char`、布尔型`boolean`
- 引用数据类型:字符串`String`
##### 6、关键字、标识符(驼峰式写法)
##### 7、方法(接受数据进行处理返回一个处理后的结果)
- 方法可以重载:出现多个方法名称相同、但是形参列表不同
##### 8、类型转换
- 自动类型转换(小范围赋值给大范围)、强制类型转换(大范围赋值给小范围)
- 表达式种的自动类型转换(表达式种小范围类型的变量会自动转换成表达式中最大范围类型的变量,最终结果类型由最高类型决定),表达式中,byte、short、char会直接转换成int参与运算。
##### 9、输入输出
输出`System.out.println("Hello World!")(默认换行)`
`System.out.print()(默认不换行)`
输入:通过java提供的Scanner程序来实现,
`Scanner sc = new Scanner(System.in)`
##### 10、运算符
- 算数运算符`(+-*/%)`,自增自减运算符`(++a 先加再用,a++先用再加)`、赋值运算`(a+=b,扩展的赋值运算符隐含了强制类型转换)`、关系运算符(三元运算符)、逻辑运算符`(& | ! ^(异或,同假异真))` `&& ||的话,前面能给出结果后面直接不执行`
##### 11、程序流程顺序
- 顺序结构、分支结构`if,switch`、循环结构`for, while, do-while`
##### 12、分支结构
- `if分支结构`:`if(条件){}else if(条件){}else{}`
- `switch分支结构`:`switch(表达式){case 值:...;break; ...; default: ...}`
- switch的注意事项、穿透性的应用:(1)表达式类型不支持double、float、long(无法精确判断值);(2)case给出的值不允许重复、且只能是字面量不能是变量;(3)正常使用switch的时候,不要忘记break,否则会出现穿透现象。
##### 13、循环结构
- for循环格式:`for(int i = 0; i < 3; i ++){}`
- while循环格式:`while(条件){};`不满足条件不执行括号内容。
- do-while循环格式:`do{}while(条件);`(一定会执行一次)
- 死循环:`for(;;)` `while(true){}` `do{}while(true)`
- 循环嵌套:循环中包含循环
- break(跳出循环)和continue关键字(跳出本次循环)。
##### 14、数组:
- 静态初始化`int[] nums= new int[]{1,2,3}`, 长度为 `nums.length`
- 动态初始化`int[] nums = new int[10]`,
- 默认值:`大部分是0或0.0;boolean是false;类、接口、数组、String默认值是null`
##### 15、二维数组:可以理解为一个值为数组的数组。
`String[][] nums = {{...},{...},{...},...}`
`String[][] arr = new int[3][5]`
`arr.length = 3(默认为行数)`
#### 2 面向对象编程(封装、继承、多态)
##### 1、啥是对象?:
- 对象是一种特殊的数据结构,可以用来记住一个事物的数据,从而代表该事物。
- `对象的设计图:类`;`类名 对象名 = new 类名();`
- 可以简单理解为一个表。
- 计算机内部分为方法区、栈内存、堆内存。每次new一个对象,都会在堆内存中开辟一个空间保存该对象的数据,对象的方法保存在方法区。栈中的对象元素实际上是对象在堆内存中的地址,堆内存中保存方法在方法区中的地址,从而访问方法。
##### 2、类的基本语法 - 构造器:
- 没有返回值类型,且方法名与类名一致。分为有参构造器和无参构造器。
- 创建对象时,对象会自动调用构造器。同时完成对象成员的初始化赋值。
- 类默认自带一个无参构造器;但是如果类定义了有参数构造器,类默认的无参数构造器就没有了,需要再手写一个无参数构造器。
##### 3、类的基本语法 - this关键字:
- 就是一个变量,用在方法中,来拿到当前对象。值是该对象的地址。(哪个对象调用方法,this就指向哪个对象)
- this主要用来解决:变量名称冲突问题的。(对象内的成员变量名与局部变量名冲突)
##### 4、**类的基本语法 - 封装**:面向对象三大特征之一;
- 封装的设计要求:合理隐藏、合理暴露。
- `private`:隐藏,只能在本类中直接访问。
- `public`:暴露,用public修饰的get和set方法合理暴露。
##### 5、 类的基本语法 - 实体类:
- 要求:(1)类中的成员全部私有,且提供public修饰的getter/setter方法;(2)类中需要提供一个无参数构造器。
- 实体类的对象只负责数据存取,而对数据的业务处理交给其他类的对象来完成,以实现数据和数据业务处理相分离。
##### 6、 类的基本语法 - static关键字:
- 叫静态,可以修饰成员变量、成员方法。
- 静态变量(类变量):有static修饰,属于类的。在计算机中只有一份,会被类的全部对象共享。
- 实例变量(对象的变量):无static修饰。属于每一个对象的。
- 静态变量的应用场景:如果某个数据只需要一份,且希望能够被共享(修改、访问)、则该数据可以定义为静态变量。
##### 7、(续6)static修饰方法
- 静态方法:有static修饰,属于类。
- 实例方法:无static修饰,属于对象。
- 何时用何方法:如果这个方法只是为了做一个功能且不需要直接访问对象的数据,这个方法直接定义为static方法。如果这个方法是对象的行为,需要访问对象的数据,这个方法必须定义为实例方法。
- 静态方法常用做工具类,调用方便,节省内存。(工具类没有创建对象的需求,建议将工具类的构造器进行私有)
##### 8、static注意事项:!!!
- 静态方法中可以直接访问静态成员,不可以直接访问实例成员。
- 实例方法中既可以访问静态成员,也可以访问实例成员。
- 实例方法中可以出现this关键字,静态方法中不可以出现this关键字。
##### 9、 **类的基本语法 - 继承**
- 提高代码的重用性,减少一些重复代码的编写。
- `public class B extends A{}`:A类被称为父类(基类或超类)、B类称为子类(派生类);
- 子类能继承父类的非私有成员;子类的对象是由子类父类共同完成的。
##### 10、继承 - 权限修饰符
- private 只能本类
- 缺省:本类、同一个包(同一个文件夹内)中的类
- protected:本类,同一个包中的类、子孙类中。
- public:任意位置。
- `private < 缺省 < protected < public`
##### 11、继承 - 继承的特点
- 单继承:java是单继承模式,一个类只能继承一个直接父类。
- 多层继承:Java不支持多继承,但是支持多层继承。
- 祖宗类:Java中所有的类都是`Object`类的子 类。
- 就近原则:变量先访问子类局部范围,再访问自己类中,自己类中的没有才会访问父类。父类也没有就报错。
- 子类访问父类:`super.xxx`
##### 12、继承 - 方法重写
- 什么时候重写:当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类可以重写一个方法名参数列表一样的方法,去覆盖父类的这个方法。这就是方法重写。
- 要加一个**注解**:`@Override` 这是方法重写的校验注解。要求方法名称和形参列表必须与被重写方法一致,否则报错。
- 子类重写父类方法时候,访问权限必须大于等于父类该方法的权限(public > protected > 缺省);返回值类型范围必须小于或等于父类该方法的返回值类型。
- 私有方法、静态方法不能被重写!
- 技巧:**声明不变,重新实现**
- 方法重写的常见场景:
- 子类重写Object类的toString()方法,以便返回对象的内容。(如果直接输出对象,默认会调用Object的toString方法,返回对象的地址信息。)
##### 13、继承 - 子类构造器
- 特点:子类的全部构造器,必须先调用父类的构造器,再执行自己。(子类构造器第一行默认会执行`super()`)
##### 14、继承 - 补充知识:this(...)调用兄弟构造器。
- 任意类的构造器中,是可以通过`this(...)`去调用该类的其他构造器的。
-
```
public A(int a){this(a, 2);}
public A(int a, int b){}
A a = new A(3)
```
- 也就是说,当对象中有默认值变量时候,为了避免重复赋值,存在多个构造器时候,可以自己调用自己的兄弟构造器减少代码量。
- 注意:`super(...)`、`this(...)`必须写在构造器的第一行,而且两者不能同时出现在同一个构造器中。
##### 15、**类的基本语法 - 多态**
- 什么是多态?:多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。
- `People p1 = new Student();`
- `people p2 = new Teacher();`
- 对于方法(必须要重写方法):编译看左边,运行看右边。(会执行右边对象的方法)
- 对于成员变量:编译看左边,运行也看左边(成员变量是People中的成员变量的值)(只有对象多态和行为多态,成员变量不多态)
##### 16、多态 - 多态的好处:
- (1)多态形式下,右边对象是解耦合的(可以卸下来,降低依赖性),更便于扩展和维护。(方便模块化)
- (2)定义方法时候,使用父类类型的形参,可以接收一切子类对象,扩展性更强,更便利。(比如在宠物游戏中,所有动物都可以送给这个方法开始跑步,不需要每一种动物设置一种判断)
- 注意:多态下不能使用子类的独有功能(需要强转)。
##### 17、多态 - 多态下的类型转换
- 自动类型转换:`父类 变量名 = new 子类()`
- 强制类型转换:`子类 变量名 = (子类)父类变量`
- 注意事项1:存在继承/实现关系就可以在编译阶段进行强制类型转换,编译阶段不会报错。
- 注意事项2:运行时,如果发现对象的真是类型与强转后的类型不同,就会报类型转换异常`ClassCastException`的错误出来。
- `People p = new Teacher();`
- `Student s = (Student)p; // java.lang.ClassCastException`
- 建议:强转前,使用`instanceof`关键字,判断当前对象的真实类型,再进行强转。
- `if(p instanceof Student){Student s = (Student)p;}`
##### 18、**编程小技巧 - lombok:**当不喜欢每定义一个类就手写getter和setter方法,可以使用lombok技术
- lombok技术可以实现为类自动添加getter setter方法、无参数构造器、toString方法等。(需要下载,注意版本)
```
@Data // 该注解可以实现为类自动添加getter setter方法、无参数构造器、toString方法等
@NoArgsConstructor // 加入无参数构造器
@AllArgsConstructor // 加入有参数构造器
```
#### 3 面向对象高级(final、单例类、枚举类、抽象类、接口)
##### 1、final关键字
final关键字可以修饰:类、方法、变量。
- 修饰类:该类被称为最终类,特点是不能被继承了。
- 修饰方法:该方法称为最终方法,特点是不能被重写了。
- 修饰变量:该变量有且仅能被赋值一次。(变量有哪些呢?成员变量和局部变量)
final修饰变量的注意:
- final修饰基本类型的变量,变量存储的**数据**不能被改变。
- final修饰引用类型的变量,变量存储的**地址**不能被改变,但是地址所指向对象的内容是可以被改变的。
##### 2、static final - 常量
使用了static final修饰的成员变量就被称为常量;常用于记录系统的配置信息。(大多用大写,且多单词之间用下划线连接)
- 程序编译后,常量会被”宏替换“:出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。
##### 3、单例类(设计模式)
什么是设计模式?一个问题通常有n种解法,其中肯定有一种解法是最优的,这个最优的解法被人总结出来了,称之为设计模式。
- 解决什么问题?
- 怎么写?
单例设计模式:
- 作用:确保某个类只能创建一个对象。
**饿汉式单例**:拿对象时,对象早就创建好了:拿对象时,对象早就创建好了。以下是步骤:
- 把类的构造器私有;确保单例类对外不能创建太多对象,单例才有可能。
- 定义一个静态变量记住类的一个对象;并把它进行私有,这样外部不能改变他的值(也可以用final)
- 定义一个静态方法,返回这个类的唯一对象。
```
public class A{
private static A a = new A();
private A(){}
public static A getObject(){return a;}
}
```
**懒汉式单例**:用对象时,才开始创建对象。步骤如下;
- 把类的构造器私有
- 定义一个静态变量存储对象
- 提供一个静态方法,保证返回的是同一个对象。
-
```
public class B{
private static B b; // null
private B(){}
public static B getObject(){
if(b == null){b = new B();}
return b;
}
}
```
##### 4、枚举类enum
(1)认识枚举类
枚举是一种特殊类。写法是`public enum A{X, Y, Z; ...}`
上述枚举类经过反编译后得到是:
```
public final class A extends java.lang.Enum{
public static final A X = new A();
public static final A Y = new A();
public static final A Z = new A();
private A(){}
public static A[] values();
public static A valueOf(java.lang.String);
}
```
- 枚举类都是最终类,不可以被继承,枚举类都是继承java.lang.Enum类的。
- 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量会记住枚举类的一个对象。
- 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。(枚举类都是多例类)
- 编译器为枚举类新增了几个方法。(访问索引用`a.ordinal()`)
(2)枚举类的常见应用场景
- 枚举类很适合做信息的分类和标志。
##### 5、抽象类 - abstract
(1)认识抽象类
- abstract修饰类,这个类就是抽象类;
- abstract修饰方法,这个方法就是抽象方法。
```
public abstract class A{
//抽象方法:必须abstract修饰,只有方法声明,不能有方法体{}。
public abstract void test();
}
```
- 抽象类中不一定要有抽象方法,有抽象方法的类必须是抽象类。
- 类有的成员:成员变量、方法、构造器、抽象类都可以有。
- **抽象类最主要的特点**:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
- 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义为抽象类。
- 抽象类的使命就是被子类继承。
(2)使用抽象类的好处
父类知道每一个子类都要做某个行为,都是每个子类要做的情况不一样,父类就定义成抽象方法,交给子类去重写实现。
- 我们设计这样的抽象类,就是为了更好的支持多态。
(3)**模板方法设计模式**
提供一个方法作为完成某类功能的模板,模板方法封装了每个实现步骤,但是允许子类提供特定步骤的实现。
模板方法设计模式:提高代码的复用,并简化子类设计。
写法:
- 1 定义一个抽象类
- 2 在里面定义2个方法
- 一个是模板方法:把共同的实现步骤放里面去。
- 一个是抽象方法:不确定的实现步骤,交给具体的子类来完成。
建议用final关键字来修饰模板方法。(因为模板方法不能被重写)(模板是给用的,不能被重写)
##### 6、接口interface
(1)接口概述
- Java提供了一个关键字interface定义出接口。
```
public interface 接口名{
int a = 1;// 成员变量(常量)(定义时候可以省略public static final不写,默认会加上去)
void run();// 成员方法(抽象方法)(定义时候可以省略public abstract不写,默认会加上去。
}
```
- 接口不能创建对象。
- 接口是用来被类实现(implements)的,实现接口的类称为实现类,一个类可以同时实现多个接口。
```
修饰符 class 实现类类名 implements 接口1, 接口2,...{
// 实现类实现多个接口,必须重写完全部抽象方法,否则实现类需要定义为抽象类。
}
```
(2)接口的好处
- 弥补了单类继承的不足,一个类同时可以实现多个接口,使类的角色更多,功能更强大。
- 让程序可以面向接口编程,这样程序员就可以灵活方便的切换各种业务实现(更利于程序的解耦合)。
##### 7、接口 - 其他内容
(1)接口新增的三种方法
jdk8开始,接口新增了三种形式的方法:
```
public interface A{
// 1 默认方法(实例方法):使用default修饰,默认会被加上public修饰。
// 注意:只能使用接口的实现类对象调用。
default void test1(){}
// 2 私有方法:必须用private修饰(JDK 9开始才支持)
// 注意:只能使用接口中的其他实例方法来调用它。
private void test2(){}
// 3 类方法(静态方法):使用static修饰,默认会被加上public修饰。
// 注意:只能用当前接口名来调用。
static void test3(){}
}
```
上述三种方法,自己基本不会用。主要是会看懂就行。
作用:增强了接口的能力,更便于项目的扩展和维护。(比如接口中新增一个功能,不需要再修改实现类。)
(2)接口的注意事项
- 接口与接口可以多继承,一个接口可以同时继承多个接口。【重点】【相当于合并多个接口】
- 一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承,也不支持多实现。【了解】【接口将继承的同名方法合并为一个抽象方法了,但是如果签名冲突,比如返回值参数不一样,就不支持多继承和多实现了】
- 一个类继承了父类,又同时实现了接口,如果父类中和接口中有同名的默认方法,实现类会优先调用父类的。【如果硬要用某个接口中的方法,则重写该方法,`接口名.super.方法();`】
- 一个类实现了多个接口,如果多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
##### 8、抽象类和接口的区别
相同点:
- 都是抽象形式,都可以有抽象方法,都不能创建对象。
- 都是派生子类形式,抽象类是被子类继承使用,接口是被实现的。
- 继承抽象类或者实现接口,都必须重写完他们的抽象方法,否则自己要成为抽象类或者报错。
- 都能支持多态,都能够实现解耦合
不同点:
- 抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法(jdk8新增的三种方式)。
- 抽象类只能被类单继承,接口可以被类多实现。
- 一个类继承抽象类就不能再继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)。
- 抽象类体现模板思想,更利于做父类实现代码的复用性。(最佳实践)
- 接口更适合做功能的解耦合,解耦合性更强更灵活。(最佳实践)
#### 4 进一步面向对象高级(代码块、内部类、函数式编程、常用API、GUI编程)
##### 1、代码块 `static{},{}`
代码块是类的5大成分之一(成员变量、构造器、方法、代码块、内部类)
代码块分为两种:
(1)静态代码块:属于类
- 格式:`static{}`
- 特点:类加载器自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
- 作用:完成类的初始化,例如:对静态变量的初始化赋值。
静态代码块与类一起**优先加载**,自动执行一次。可以完成对类的静态资源的初始化。
(2)实例代码块:属于对象
- 格式:`{}`
- 特点:无static修饰,每次创建对象时,执行实例代码块,并在构造器前执行。
- 作用:和构造器一样,都是用来完成对象的初始化的,例如:对实例变量进行初始化赋值。
实例代码块是在每次创建对象时候优先执行一次,**不创建对象不执行**。
##### 2、内部类(会看懂别人的代码)
**(成员内部类、静态内部类、局部内部类、匿名内部类)**
如果一个类定义在另一个类的内部,这个类就是内部类。
- 场景:当一个类的内部,包含了一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
**2-1 成员内部类**:就是类中的一个普通成员,类似普通的成员变量、成员方法。
**属于外部类的对象持有**
```
public class Outer{
// 成员内部类
public class Inner{}
}
// 外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称()
Outer.Inner oi = new Outer.new Inner();
```
成员内部类访问外部类成员的特点(扩展):
```
// 1、成员内部类中可以直接访问外部类的静态成员。也可以直接访问外部类的实例成员。
// 2、成员内部类的实例方法中,可以直接拿到当前寄生的外部类对象:外部类名.this
```
**2-2 静态内部类**:
有static修饰的内部类,**属于外部类本身持有。**
```
public class Outer{
// 静态内部类
public static class Inner{}
}
// 外部类名.内部类名 对象名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
```
静态内部类访问外部类成员的特点
```
// 1、静态内部中是否可以直接访问外部类的静态成员?可以!
// 2、静态内部类中是否可以直接访问外部类的实例成员?不可以!(外部类的实例变量属于外部类的对象)
```
!!!成员内部类属于外部类的对象,只有创建对象才能调用内部类。而静态内部类直接属于类本身,不需要创建外部类的对象可以直接调用内部类。那么所有属于外部类对象的成员,静态内部类都不能访问。
(自己开发中,自己99%不会用内部类)
2-3 局部内部类(了解)
局部内部类是定义在方法中、代码块中、构造器等执行体中。
##### 3、匿名内部类(重点!)
- 是一种特殊的局部内部类;
- 所谓匿名:指的是程序员不需要为这个类声明名字,默认有个隐藏的名字。`Test$1.class`
```
new 类或接口(参数值){类体(一般是方法重写);}
new Animal(){
@Override
public void cry(){}
}
```
- 特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
- 作用:用于更方便的创建一个子类对象。
*匿名内部类常见使用形式:*
- 通常作为一个对象参数传输给方法。
*应用场景*
- 调用别人提供的方法实现需求时,这个方法正好可以让我们传输一个匿名内部类对象给其使用。{比如我们需要传一个接口类型的参数,都是接口类型不能直接定义对象,而需要创建一个子类来实现这个接口。为了方便,我们可以直接定义匿名内部类作为参数进行传递}
- 使用comparator接口的匿名内部类实现对数组的排序。
```
A[] a = new A[10];
Arrays.sort(a, new Comparator(){
@Override
public int compare(A i1, A i2){
return i1.x - i2.x;
}
});
// public static void sort(T[] a, Comparator c)
// 参数1:待排序的对象
// 参数2:排序的规则
```
##### 4、函数式编程 - Lambda
- 此”函数“类似于数学中的函数(强调做什么),只要输入的数据一致,返回的结果也是一致的
- 使用Lambda函数替代某些匿名内部类对象,从而让程序更简洁,可读性更好。
- 注意:Lambda表达式只能替代**函数式接口**的匿名内部类!!!
- 函数式接口?有且仅有一个抽象方法的接口。
```
(被重写方法的形式参数列表) -> {被重写方法的方法代码。}
@FunctionalInterface // 声明函数式接口的注解
interface Swim{
void swimming();
}
Swim s1 = new Swim(){
@Override
public void swimming(){...}
}
// 以下是Lambda函数
Swim s2 = () -> {...};
```
- 理解:通过上下文推断,上文推断出是Swim接口,下文推断出Swim接口的唯一方法。
案例1:用Lambda函数简化实际示例。
```
A[] a = new A[10];
Arrays.sort(a, (A a1, A a2)->{return a1.x - a2.x;});
```
还能进一步简化Lambda表达式的写法
- 参数类型全部省略不写
- 如果只有一个参数,参数类型省略的同时"()"也可以省略,但是多个参数不能省略
- 如果Lambda表达式中只有一行代码,大括号可以不写,同时要省略分号";",如果这行代码是return语句,也必须去掉return。
```
A[] a = new A[10];
Arrays.sort(a, (a1, a2) -> a1.x - a2.x);
```
##### 5、函数式编程 - 方法引用(进一步简化Lambda)
静态方法的引用、实例方法的引用、特定类型方法的引用、构造器引用
(1)静态方法引用:
- **类名::静态方法**
- 使用场景:如果某个Lambda表达式只是**调用一个静态方法**,并且"->"**前后参数的形式一致**,就可以使用静态方法引用。
```
A[] a = new A[10]
// 匿名内部类
Arrays.sort(a, new Comparator(){
@Override
public int compare(A a1, A a2){
return a1.x - a2.x;
}
});
// Lambda
Arrays.sort(a, (A a1, A a2)->{return a1.x - a2.x;});
// Lambda简化版
Arrays.sort(a, (a1, a2) -> a1.x - a2.x);
// A中定义一个静态方法。public static int compare(A a1, A a2)
// 简化最终版:静态方法引用
Arrays.sort(a, A::compare);
```
(2)实例方法引用
- **对象名::实例方法**。
- 使用场景:如果某个Lambda表达式里只是**通过对象名称**调用一个实例方法,并且"->"**前后参数的形式一致**,就可以使用实例方法引用。
(3)特定类型方法的引用(主要是String)
- **特定类的名称::方法**
- 使用场景:如果某个Lambda表达式里只是调用一个特定类型(如String)的实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时可以使用特定类型方法的引用。
```
String[] names = {...};
// java已经为我们提供了字符串按照首字母忽略大小写比较的方法:comparaToIgnoreCase
Arrays.sort(names, new Comparator(){
@Override
public int compare(String o1, String o2){
return o1.comparaToIgnoreCase(o2);
}
});
// Lambda方法
Arrays.sort(names, (o1, o2) -> o1.comparaToIgnoreCase(o2));
// 特殊类型方法的引用
Arrays.sort(names, String::comparaToIgnoreCase);
```
(4)构造器引用
- **类名::new**
- 使用场景:如果某个Lambda表达式里只是在创建对象,并且"->"前后参数情况一致,就可以使用构造器引用。
```
// 这个案例是硬凑的,主要是为了体现构造器引用。
interface CarFactory{
Car getCar(String name);
}
class Car implements CarFactory{...}
CarFactory cf = name -> new Car(name);
简化后:
CarFactory cf = Car:new;
```
##### 6、常用API(String、ArrayList)
(1)String
代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理。
- 创建字符串对象,封装字符串数据
```
// 方式1:直接""创建对象
String s1 = "123";
// 其他方式不推荐。。。
String s1 = new String("123");
```
- 区别:只要以"..."写出的字符串对象,会存储到字符串常量池(存在堆内存中),且相同内容的字符串只会存储一份。更节省内存。
- 通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
(2)ArrayList
集合是一种容器,用来装数据,类似于数组。
- 数组定义完成启动后,长度就固定了。
- 集合大小可变,功能丰富,开发中用的更多。
- 还有set、map等集合。
```
ArrayList list = new ArrayList<>();
list.add("xxx");
list.get(index);//取数据
list.remove(index);//删除数据,会返回数据
list.remove(内容);//删除指定内容,返回bool值
```
##### 7、GUI编程(锻炼技术、企业不用、简单了解)
全称:Graphical User Interface,是指图形用户界面
- 通过图形元素(如窗口、按钮、文本框等)与用户进行交互。
- 与命令行界面(CLI)相比,GUI更加直观、友好。
Java的GUI编程包:
- AWT(Abstract Window Toolkit)(被淘汰)
- 提供了一组原生的GUI组件,依赖于操作系统的本地窗口系统。(依赖操作系统)
- Swing
- 基于AWT,提供了更丰富的GUI组件,轻量级组件,不依赖本地操作系统窗口。(windows上能跑,mac上也能跑)
- 常用的Swing组件
```
JFrame:窗口
JPanel:用于组织其他组件的容器
JButton:按钮组件
JTextField:输入框
JTable:表格
```
- 常见的布局管理器
- 布局管理器(Layout Manager)它们可以决定组件在容器中的布局方式,避免了手动设置每个组件的位置和大小,从而简化了GUI设计过程。
```
FlowLayout // 水平布局、垂直布局
BorderLayout // 方向布局(东南西北中)
GridLayout // 网格布局
BoxLayout // 也可以方位布局(沿着x轴或着y轴排序组件)
```
GUI事件处理
- 点击事件监听器ActionListener
- 按键事件监听器KeyListener
- 鼠标事件监听器MouseListener
事件的几种常见写法
- 第一种:直接提供实现类,用于创建事件监听对象
- 第二种:直接使用匿名内部类的对象,代表事件监听对象
- 第三种:自定义窗口,让窗口对象实现事件接口。
```
比如
public class LoginFrame extends JFrame implements ActionListener{...}
LoginFrame lf = new LoginFrame();
lf.setVisible(true);
```
- GUI窗口初始化
```
// 设置窗口的标题
this.setTitle("xxx");
// 设置窗口的宽高
this.setSize(500, 500);
// 设置窗口的关闭方式
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//设置窗口的居中显示
this.setLocationRelativeTo(null);// 相对于中心点的位置
// 设置窗口的显示
this.setVisible(true);
```
#### 5 javaSE基础加强
- 异常、泛型、集合框架、Stream流、File、字符集、IO流框架、多线程、网络编程、综合项目、java高级项目
##### 1、 异常
异常代表程序出现的问题
- 读取的文件不存在了
- 读取网络数据,断网了
- ...
**01 Java的异常体系**
```
Java.lang.Throwable
|-Error
// Error代表的系统级别的错误(属于严重问题),也就是说系统一旦出现问题,sun公司会把这个问题封装成Error对象给出来(不是给程序员用的,开发人员不用管)
|-Exception
// Exception代表的我们程序可能出现的问题
|- RuntimeException
|- 其他异常
- 运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常。(比如:数组索引越界异常)
- 编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
```
**02 异常的基本处理**
- 抛出异常(`throws`)
- 在方法上使用throws关键字,可以将方法内部出现的异常抛出去给调用者处理。
- 捕获异常(`try{}catch(){}`)
- 直接捕获程序出现的异常
```
方法 throws 异常1,异常2,...{}
try{
// 监视可能出现异常的代码!
}catch(异常类型1 变量){
//处理异常
}catch(异常类型2 变量){
//处理异常
}...
```
**03 异常的作用**
- 作用1: 异常是用来定位程序bug的关键信息
- 作用2: 可以作为方法内部的一种特殊返回值,以便通知上层调用者,方法的执行问题。
- 当某个方法可能出现异常,并且没有办法通过正常方法返回值,可以返回一个异常。
```
try{
div(10, 0);
// 正确执行div的结果
}catch(Exception e){
e.printStackTrace();
// div抛出异常的处理
}
int div(int a,int b)throws Exception{
if(b == 0){
throw new Exception("除数不能为0");
}
return a / b;
}
```
**04 自定义异常**
Java无法为这个世界上全部的问题都提供异常来代表,如果企业自己的某种问题,想通过异常来表示,以便异常来管理该问题,那就需要自己来定义异常类。
- 自定义运行时异常:定义一个异常类继承`RuntimeException`
- 自定义编译时异常:定义一个异常类继承`Exception`
编译时异常:反馈很强烈。现在的java已经逐渐摒弃编译时异常。
**05 异常的处理方案**
- 方案1:底层异常层层往上抛出,最外层捕获异常,记录下异常信息,并相应适合用户观看的信息进行提示。
```
// 推荐方式
方法 throws Exception{}
// Exception代表可以捕获一切异常。
```
- 方案2:最外层捕获异常后,尝试重新修复。 (如果不捕获,出现异常会直接结束程序。)
```
public static void main(String[] args) {
System.out.println("程序开始...");
while (true) {
try{
double price = getPrice();
System.out.println("商品价格是:" + price);
break;
} catch (Exception e) {
System.out.println("价格输入错误!");
}
}
System.out.println("程序结束...");
}
public static double getPrice() {
System.out.println("请输入商品价格:");
Scanner sc = new Scanner(System.in);
return sc.nextDouble(); // 可能输入其他内容
}
```
##### 2、泛型
定义类、接口、方法时,同时声明了一个或多个类型变量(如:``)。称为泛型类,泛型接口,泛型方法,它们统称为泛型。
```
public class ArrayList{...}
```
- 作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力。
- 这样可以避免强制类型转换,以及可能出现的异常。
```
ArrayList list = new ArrayList();
list.add(1);
list.add("p");
list.add(true);
list.add(new Object());
int i = 1
Object rs = list.get(i);
// 强制类型转换会出问题
String s = (String)rs;
改进======
ArrayList list = new ArrayList();
```
- **泛型的本质**:把具体的数据类型作为参数传给类型变量。
**01 泛型类**
```
修饰符 class 类名<类型变量1,类型变量2>{}
public class ArrayList{...}
注意:类型变量建议用大写的英文字母,常用的有:E T K V
```
**02 泛型接口**
```
修饰符 interface 接口名<类型变量,类型变量...>{}
public interface A{...}
注意:类型变量建议用大写的英文字母,常用有:E T K V
public interface Data{}
public class TeacherData implements Data{}
public class StudentData implementsData{}
```
**03 泛型方法**
```
修饰符<...> 返回值类型 方法名(形参列表){}
public static void test(T t){}
public E get(int id){}// 注意,这个不是泛型方法
String[] names;
Student[] stds;
public static void printArray(T[] a){}
public static T getMax(T[] a){}
String s = getMax(names); // 返回值不需要强转
Student n = getMax(stds);
```
**04 通配符**
就是`?`,可以在“使用泛型”的时候代表一切类型;E T K V是在定义泛型的时候使用。
```
ArrayList xiaomis = new ArrayList<>();
ArrayList byds = new ArrayList<>();
// Xiaomi 和 BYD继承Car
// 但是ArrayList,ArrayList,ArrayList没有关系
public static void go(ArrayList cars) // 只能用Xiaomi
public static void go(ArrayList cars) // Xiaomi和BYD都不能用,只能用Car
应用>
public static void go(ArrayList> cars) //都可以用。
但是不能所有的类的集合都能用,只能让Car的子类用,这需要了解泛型的上下限。
```
**泛型的上下限**
- 泛型上限:`?extends Car` : ?能接收的必须是Car或者其子类。
- 泛型下限:`?super Car`:?能接收的必须是Car或者其父类。
- `public static void go(ArrayList extends Car> cars) `
**05 泛型支持的类型 & 包装类**
**泛型不支持基本数据类型`(byte\short\int\long\char\float\double\boolean)`,只能支持对象类型(引用数据类型)。**
```
ArrayList list = new ArrayList<>(); //会报错
```
- 泛型擦除:泛型工作在编译阶段,等编译后泛型就没用了,所以泛型在编译后都会被擦除,所有类型都会恢复成`Object`类型。
- 而`Object`类型是不支持基本数据类型,比如`Object o = 12`是错误的。
为解决上述方法,引入了**包装类**
包装类就是把基本类型的数据包装成对象的类型。属于引用数据类型。
```
byte -> Byte
short -> Short
int -> Integer
long -> Long
char -> Character
float -> Float
double -> Double
boolean -> Boolean
```
把基本数据类型变成包装类对象(面试可能问)
```
// Integer i = new Integer(100); // idk9后就过时了
Integer i = Integer.valueOf(100);
// public static Integer valueOf(int value)
// 为什么会过时,这个方法已经封装了-128-127之间的所有对象,大家共享同一个对象,不需要额外new新的对象。
```
**自动装箱**:基本数据类型的数据可以直接变成包装对象的数据,不需要额外做任何事情。
`Integer it = 100;`
**自动拆箱**:把包装类型的对象直接给基本类型的数据。
`int i = it;`
```
ArrayList list = new ArrayList<>();
list.add(123);
// 这个123不是基本数据类型,而是自动装箱成了Integer类型的对象。
int rs = list.get(i); // 自动拆箱
```
**包装类具备的其他功能**
- 可以把基本类型的数据转换成字符串类型。
- `public static String toString(double d)`
- `public String toString()`
- 可以把字符串类型的数值转换成数值本身对应的真实数据类型。
- `public static int parseInt(String s)`
- `public static Integer valueOf(String s)`
```
String str = "98";
int i = Integer.parseInt(str);
System.out.println(i + 2); // 100
int j = Integer.valueOf(str);
System.out.println(j + 2); // 100
```
##### 3、集合框架(单列集合Collection、双列集合Map)
集合是一种容器,用来装数据的,类似于数组,但是集合的大小可变,开发中也非常常用。
`Collection` `List` `ArrayList` `LinkedList` `LinkedHashSet` `Map` `HashMap` `TreeMap` `LinedHashMap` `Set` `HashSet` `TreeSet`
**集合分类**
- `Collection` 单列集合:collection代表单列集合,每个元素(数据)只包含一个值。
- `Map` 双列集合:Map代表双列集合,每个元素包含两个值(键值对)
**集合体系结构**
- `Collection`
- 接口
- 实现类
- **` List`** 添加的元素是有序、可重复、有索引。
- `ArrayList`
- `LinkedList`
- `Set` 添加的元素是无序、不重复、无索引。
- `HashSet` :无序、不重复、无索引。
- `LinkedHashSet` :有序、不重复、无索引。
- `TreeSet` :按照大小默认升序排序、不重复、无索引。
```
ArrayList s = new ArrayList<>();
s.add("java");
s.add("java");
s.add("hello");
HashSet hs = new HashSet<>();
hs.add("java");
hs.add("java");
hs.add("hello");
// s: ["java", "java", "hello"]
// hs:["hello", "java"]
```
##### 4、Collection的常用功能
`Collection` 是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的。
**Collection常用功能**
```
public boolean add(E e) // 把给定的对象添加到当前集合中
public void clear() // 清空集合中所有的元素
public boolean remove(E e) // 把给定的对象在当前集合中删除
public boolean contains(Object obj) // 判断当前集合中是否包含给定的对象
public boolean isEmpty() // 判断当前集合是否为空
public int size() // 返回集合中元素的个数
public Object[] toArray() // 把集合中的元素,存储到数组中。
```
**Collection遍历方式一 - 迭代器遍历**
- 迭代器是用来遍历集合的专用方式(数组没有迭代器)
- `Iterator iterator() :返回集合中的迭代器对象`
- `Iterator it = s.iterator(); `
- 常用方法
- `boolean hashNext()` :询问当前位置是否有元素存在,存在返回`true`
- `E next()` :得到当前位置的元素,并同时将迭代器对象指向下一个元素处。
**Collection的遍历方式二 - 增强for循环**
```
for(元素的数据类型 变量名 : 数组或集合){}
Collection c = new ArrayList<>();
...
for(String s : c){
System.out.println(S);
}
```
- 增强for可以用来遍历集合或数组。
- 增强for遍历集合,本质就是迭代器遍历集合的简化写法。
**Collection的遍历方式三 - Lambda表达式**
- 得益于JDK 8开始的新技术Lambda表达式,提供了一种更简单、更直接的方式来遍历集合。
```
default void forEach(Consumer super T> action) // 结合lambda遍历集合
Collection lists = new ArrayList<>();
...
lists.forEach(s->{
System.out.println(s);
});
// lists.forEach(s->System.out.println(s));
```
##### 5、三种遍历方式的区别(迭代器、for增强、lambda)
认识并发修改异常问题
- 遍历集合的同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称之为**并发修改异常问题**。
```
ArrayList list = new ArrayList<>();
// 通过增加,list = ["1", "2", "1", "1", "2"]
// 删除所有1的数据
for(int i = 0; i < list.size(); i ++){
String s = list.get(i);
if(s.equals("1"))list.remove(s);
}
// 删除后是 list = ["2", "1", "2"]
// 1 2 1 1 2(i=0) -> 2 1 1 2(i=0)
// 2 1 1 2(i=1) -> 2 1 2(i = 1)
// 2 1 2(i=2) -> 2 1 2(退出去了)
// 解决办法
// 法1:每次删除要i--;
// 法2:倒着遍历并删除(前提是支持索引)。
```
- 迭代器循环:存在并发修改异常。
- 原因:每次`it.next()`后会进入迭代器的`next`方法,其中有一个检查是否修改的方法,在这个方法内会检查期待修改的值和默认修改值,一旦这两个修改值不一样,就会抛出异常。
- 方法:利用迭代器内部的`remove`方法,`it.remove`。这个方法每次删除后会更新期待修改值。
- `for增强`和`Lambda`都无法解决并发修改异常
- `增强for`无法找到迭代器
- 而`Lambda`的本质就是基于`for`增强
- 需要遍历并修改只能用迭代器。
##### 6、List集合(ArrayList、LinkedList)
List集合因为支持索引,所以多了很多与索引相关的方法(Collection的功能已经被List继承了)
```
void add(int index, E element) // 在此集合的指定位置插入指定的元素
E remove(int index) // 删除指定索引处的元素,返回被删除的元素
E set(int index, E element) // 修改指定索引处的元素,返回被修改的元素
E get(int index) // 返回指定索引处的元素
```
- 遍历方式:List集合因为有索引,所以可以进行for通过索引循环。
##### 7、ArrayList底层原理 和 LinkedList底层原理
两者底层采用的数据结构不同,应用场景不同
- `ArrayList`底层是基于数组存储数据的。
- 数组的特点:
- 查询速度快(根据索引查询数据快):查询数据通过地址值和索引定位,查询任意数据耗时相同。
- 增删数据效率低:可能需要把后面很多的数据进行前移或者扩容。
- `LinkedList`底层是基于**双向链表**存储数据的。
- 链表的特点:
- 链表中的数据是一个一个独立的结点组成的,结点在内存中是不连续的,每个结点包含数据值和下一个结点的地址。
- 查询慢,无论查询哪个数据都要从头开始找。
- 链表增删相对快。
- 双向链表的特点:对首尾元素进行增删改查的速度是极快的。
```
LinkedList新增了:很多首尾操作的特有方法
public void addFirst(E e)
public void addLast(E e)
public E getFirst()
public E getLast()
public E removeFirst()
public E removeLast()
可以用来设计队列和栈
```
##### 8、Set集合
无序、添加的数据的顺序和获取出的数据顺序不一致;不重复;无索引;
- `HashSet` 无序、不重复、无索引
- `LinkedHashSet` 有序、不重复、无索引
- `TreeSet` 排序、不重复、无索引
##### 9、HashSet集合的底层原理
**哈希值**
- 就是一个int类型的随机值,Java中每个对象都有一个哈希值。
- Java中的所有对象,都可以调用Object类提供的hashCode方法,返回该对象自己的哈希值。
- `public int hashCode()` :返回对象的哈希码值
**对象哈希值的特点**
- 同一个对象多次调用hashCode方法返回的哈希值是相同的。
- 不同的对象,它们的哈希值大概率不相等,但也有可能会相等(**哈希碰撞**)。
- `int(-21亿多 - 21亿多) 但是可能创建45亿个对象,哈希值不够用肯定会冲突。`
**HashSet集合的底层原理**
- 是基于哈希表存储数据的。
**哈希表**
- JDK8之前,哈希表 = 数组 + 链表
- JDK8开始,哈希表 = 数组 + 链表 + 红黑树
- 哈希表是一种增删改查数据,性能都比较好的数据结构。
```
Set set = new HashSet<>();
set.add("数据1");
1) 创建一个默认长度为16的数组,默认加载因子为0.75,数组名table
2) 使用元素的哈希值对数组的长度做运算计算出应存入的位置。(求余)
3) 判断当前位置是null,如果是null直接存入
4) 如果不为null,表示有元素,则调用equals方法比较。
5) 相等,则不存;不相等,则存入数组。
1) JDK 8 之前,新元素存入数组,占老元素位置,老元素挂下面
2) JDK 8 之后,新元素直接挂在老元素下面。
6) 一旦table中存储的数据超过 16 * 0.75 = 12后会开始扩容。(0.75是加载因子)
1) JDK 8 开始,当链表长度超过8,且数组长度 >= 64时,自动将链表转成红黑树。
```
**红黑树**
- 二叉查找树存在的问题:当数据已经是排好序的,导致查询的性能与点链表一样,查询速度变慢。
- 平衡二叉树:在满足查找二叉树的大小规则下,让树尽可能矮小,以此提高查数据的性能。(左右子树高度差不超过1)
- 红黑树:就是可以自平衡的二叉树。
- 红黑树是一种增删改查数据性能相对都较好的结构。
**HashSet集合对象元素的去重操作**
- 当几个对象的成员变量相同时,HashSet还是认为对象不一样,无法去重。
- 结论:如果希望Set集合认为2个内容一样的对象是重复的,必须重写对象的`hashCode()` 和 `equals()` 方法。
##### 10、ListedHashSet集合的底层原理
- 有序、不重复、无索引;依然基于哈希表(数组、链表、红黑树)实现
- 但是,它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。
- 牺牲内存去获取一些性能。
##### 11、TreeSet集合
- 特点:不重复、无索引、可排序(默认升序排序,按照元素的大小,由小到大排序)。
- 底层是基于红黑树实现的排序。
注意:
- 对于数值类型,Integer,Double,默认按照数值本身的大小进行升序排序。
- 对于字符串类型,默认按照首字符的编号升序排序。
- 对于自定义类型如Student对象,TreeSet是默认无法直接排序的。
- 必须自定义排序规则。
**自定义排序规则**
- 1) 对象类实现一个Comparable比较接口,重写`compareTo`方法,指定大小比较规则。
- `t2.compareTo(t1)`:如果你认为左边大于右边,请返回正整数。`return this.x - o.x;`
- 2)`public TreeSet(Comparator super E> c)` 集合自带比较器Comparator对象,指定比较规则。
##### 12、Collection小结
- 如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据。
- ArrayList:底层基于数组。**(常用)**
- 如果希望记住元素的添加顺序,且增删首尾数据的清空较多?
- LinkedList:底层基于双链表实现。
- 如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快?
- HashSet:底层基于哈希表实现。**(常用)**
- 如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快?
- LinkedHashSet:底层基于哈希表和双链表。
- 如果要对元素进行排序,也没有重复元素需要存储,且希望增删改查都快?
- TreeSet集合,对于对象,需要自定义排序规则。
##### 13、Map集合
- Map集合也被叫做“ **键值对集合** ”,格式:`{key1=value1,key2=value2,key3=value3,...}`
- Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值。
**01 Map集合在什么业务场景下使用**
- 需要存储一一对应的数据时,就可以考虑使用Map集合来做。
**02 Map集合的体系**
`Map`
- 接口
- 实现类
- `HashMap`
- `LinkedHashMap`
- `TreeMap`
**03 Map集合体系的特点**
- 注意:Map系列集合的特点都是**由键决定的**,值只是一个附属品,值是不做要求的。键值可以是`null`
- `HashMap` :无序、不重复(覆盖)、无索引;(用的最多)
- `LinkedHashMap` :有序、不重复、无索引。
- `TreeMap` : 按照大小默认升序排序、不重复、无索引。
##### 14 Map集合常用方法和遍历方法
**01 Map集合的常用方法**
```
public V put(K key, V value) 添加元素
public int size() 获取大小
public void clear() 清空集合
public boolean isEmpty() 判断是否为空
public V get(Object key) 根据键获取对应值
public V remove(Object key) 根据键删除整个元素
public boolean containsKey(Object key) 判断是否包含某个键
public boolean containsValue(Object value) 判断是否包含某个值
public Set keySet() 获取全部键的集合
public Collection values() 获取Map集合的全部值
```
```
// 遍历
for(String key: map.keySet()){
System.out.println(key);
}
```
**02 Map集合的遍历方式(键找值、键值对、Lambda)**
- 键找值:先获取Map集合全部的键,再通过遍历键来找值。
- 键值对:把“键值对”看成一个整体进行遍历(难度较大)
- Lambda:JDK1.8开始之后的新技术(非常简单)
01键找值:
```
// keyset() get(key)
for(String key : map.keySet()){
System.out.println(map.get(key))
}
```
02键值对:
```
// 把“键值对”看成一个整体进行遍历
// Set> entrySet() 获取所有“键值对”的集合。
Set< Map.Entry > entries = map.entrySet();
Entry对象 : Map.Entry
for(Map.Entry entry : enties){
String key = entry.getKey();
double value = entry.getValue();
}
```
03Lambda
`default void forEach(BiConsumer super K, ? super V> action)`
```
map.forEach((k, v) -> {
System.out.println(k + "------>" + v);
});
// 分析源码
BiConsumer super K, ? super V> action需要重写accept方法
map.forEach(new BiConsumer(){
@Override
public void accept(String key, Integer value){
System.out.println(key + "=" + value);
}
});
forEach内部使用的是键值对的方法。
```
##### 15 Map集合的实现类(HashMap、LinkedHashMap、TreeMap)
- 01 - HashMap集合的底层原理:实际上、原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。
- HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。
- 02 - LinkedHashMap的底层原理:实际上,原来学习的LinkedHashSet集合的底层原理就是LinkedHashMap。
- 底层数据结构依然是基于哈希表实现的,只是每个键值对元素又额外的多了一个双链表的机制记录元素顺序(保证有序)
- 03 - TreeMap的底层原理:TreeMap跟TreeSet的集合底层原理一样,都是基于红黑树实现的排序。只能对键排序。
- TreeMap也需要自定义排序规则。
##### 16、Stream流
- 是JDK 8 开始新增的一套API(`java.util.stream.*`),可以用于操作集合或者数组的数据。
- 优势:Stream流大量的结合了Lambda的语法风格来编程,功能强大,性能高效,代码简洁,可读性好。
**体验Stream流**
```
// 将列表中所有姓张并且名字长度为3的数据存入到一个新的集合中。
List list = new ArrayList<>();
...
List list2 = list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.collect(Collectors.toList());
```
**Stream流的使用步骤**
数据源:
- 01 获取Stream流、代表了一条流水线,并能与数据源建立连接。
- 02 调用流水线的各种方法,对数据进行处理、计算。
- 03 获取处理的结果,遍历、统计、收集到一个新集合中返回。
##### 17、Stream流的使用(三个步骤)
**01 获取Stream流**
```
01 获取集合的Stream流
default Stream stream()
//
Collection list = new ArrayList<>();
Stream s1 = list.stream();
Map map = new HashMap<>();
//可以获取键流
Stream s2 = map.keySet().stream();
// 获取值流
Stream s3 = map.values().stream();
// 获取键值对流
Stream> s4 = map.entrySet().stream();
//
// ===============================
02 获取数组的Stream流
public static Stream stream(T[] array) //获取当前数组的Stream流
public static Stream of(T... values) // 获取当前接收数据的Stream流,可变参数,可以接任意参数。
// String[] names = {...}
Stream s5 = Arrays.stream(names);
Stream s6 = Stream.of(names);
```
**02 中间方法**
中间方法指的是调用完成后会返沪新的Stream流,用完可以接着用。(链式编程)
```
Stream filter(Predicate super T> predicate) 过滤
Stream sorted()
Stream sorted(Comparator super T> comparator) 按照指定规则排序
Stream limit(long maxSize) 获取前几个元素
Stream skip(long n) 跳过前几个元素
Stream distinct() 去除流中重复的元素。
// 注意:如果自定义对象能够去重复,重写对象的hashCode和equals方法,才能去重复。
Stream map(Function super T, ? extends R> mapper) 对元素进行加工,并返回对应的新流
// 映射/加工方法:把流上原来的数据拿出来变成新数据又放到流上去。
List scores = new ArrayList<>();
scores.stream().map(s -> "加10分后:" + (s + 10));
static Stream concat(Stream a, Stream b) 合并a和b两个流为一个流。
```
**03 终结方法**
终结方法指的是调用完成后,不会返回新的Stream了,没法继续使用流了。
```
void forEach(Consumer action) 对此流运算后的元素执行遍历
long count() 统计元素个数
Optional max(Comparator super T> c) 获取最大值
Optional min(Comparator super T> c) 获取最小值
```
**收集Stream流** :就是把Stream流操作后的结果转回到集合或者数组中去返回。
- Stream流:方便操作集合/数组的手段。
- 集合/数组:才是开发中的目的。
```
R collect(Collector c) 把流处理后的结果收集到一个指定集合中
Object[] toArray() 把流处理后的结果收集到一个数组中去
=================================================
public static Collector toList() 把元素收集到List集合中去
public static Collector toSet() 把元素收集到Set集合中去
public static Collector toMap(Function KeyMapper, Function valueMapper) 把元素收集到Map集合中去
=================================================
list list1 = s1.collect(Collectors.toList());
Set set1 = s1.collect(Collectors.toSet()); // 报错
Map map = teachers.stream().collect(Collectors.toMap(t -> t.getName(), t -> t.getSalary()));
teachers.stream().collect(Collectors.toMap(Teacher::getName, Teacher::getSalary);
// 注意:流只能用一次
```
##### 18、补充:方法中可变参数、Collections工具类
**方法中可变参数**:就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:`数据类型...参数名称`
可变参数的特点和好处:
- 特点:可以不传数据给它;它可以传一个或者同时传多个数据给它;它也可以传一个数组给它。
- 好处:常常用来灵活的接收数据。
```
sum();
sum(10, 20, 30)
public static void sum(int...nums){
// 可变参数对内实际上就是一个数组,nums就是数组。nums[]
...
}
// 注意:可变参数再形参列表中只能有一个。否则不知道第一个参数接多少数据。可变参数必须放在形参列表的最后面。
```
**Collections工具类**
是一个用来操作集合的工具类。
Collections提供的常用静态方法:
```
public static boolean addAll(Collection super T> c, T... elements) 批量添加数据
public static void shuffle(List> list)打乱List集合中的元素顺序
public static void sort(List list)对List集合中的元素进行升序排序
public static void sort(List list, Comparator super T> c) 给出排序规则
```
#### 6 java加强其他重点内容
##### 1、存储&读写数据的方案(File,IO流)
**背景**
变量、数组、对象、集合等,这些数据容器都在内存中,一旦程序结束,或者断电,数据就没有了!
- 文件可以长久保存数据。
- 文件在电脑磁盘中保存,即便断电,或者程序终止,文件中的数据也不会丢失。
**File**
- File是`java.io` 包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或者文件夹)
- 注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据。
**IO流**
- 用于读写数据的(可以读写文件,或网络中的数据。。。)
##### 2、File
```
File f1 = new File(pathname);
f1.length() // 字节个数
f1.getName() // 文件名字
f1.isFile() // true or false 是否文件
f1.isDirectory() // 是否是文件夹
// 相对路径:默认是到idea工程下直接寻找文件的,一般用来找工程下的项目文件的。
// 可以创建对象代表一个不存在的文件路径。
File f2 = new File(pathname);
f2.exists() // 判断是否存在
f2.createNewFile() // 把这个文件创建出来。
// 创建文件夹
单级别:f3.mkdir()
多级别:f3.mkdirs()
// 删除文件:慎用
// 默认只能删除文件和空文件夹,删除后的文件不会进入回收站。
f5.delete()
// 可以获取某个目录下的一级文件名称
File f6 = new File(pathname); // 可定位到文件夹
String[] names = f6.list();
// 获取当前目录下所有的“一级文件对象”到一个文件对象数组中去返回(重点)
File[] files = f6.listFiles();
```
**对于`ListFiles` 方法的注意事项**
- 当主调是文件,或者路径不存在时,返回null
- 当主调是空文件夹时,返回一个长度为0的数组
- **当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回。**
- 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件。
- 当主调是一个文件夹,但是没有权限访问该文件夹时,返回null
##### 3、方法递归
**什么是递归?**
- 递归是一种算法,从形式上说,方法调用自身的形式称为方法递归(recursion)。
**递归的形式**
- 直接递归:方法自己调用自己。
- 间接递归:方法调用其他方法,其他方法又回调方法自己。
**注意**
- 递归可能出现死循环,导致出现栈内存溢出情况。
**递归算法三要素**
- 递归公式:
- 递归终结点
- 递归的方向必须走向终结点
**文件搜索**
- 案例:从D:盘中,搜索"QQ.exe"这个文件,找到后直接输出其位置
- 分析
- 1、先找出D盘下的所有一级文件对象
- 2、遍历全部一级文件对象,判断是否是文件
- 3、如果是文件,判断是否是QQ.exe
- 4、如果是文件夹,需要继续进入到该文件夹重复执行上述操作。
##### 4、字符集
**常见字符集**
标准ASCII字符集
- ASCII:美国信息交换标准代码,包括了英文、符号等。
- 使用1字节存储一个字符,首位是0,因此总共可以表示128个字符
GBK(汉字内码扩展规范,国标)
- 汉字编码字符集,包含了2万多个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。
- 注意:GBK兼容了ASCII字符集。
- 为了区分汉字和ASCII字符集,GBK规定:汉字的第一个字节的第一位必须是1
- `1xxxxxxx xxxxxxx 0xxxxxxx 1xxxxxxx xxxxxxx`
Unicode字符集(统一码,也叫万国码)
- Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
- UTF-32:每4个字节存一个字符,可以存42亿个字符。但是占存储空间大,通信效率变低。
- **UTF-8**:采取可变长编码方案
**UTF-8**
- 是Unicode字符集的一种编码方案,采取可变长编码方案,共分为四个长度区:1字节、2字节、3字节、4字节
- 英文字符、数字等只占1个字节(兼容ASCII编码),
- 汉字字符占用3个字节。
UTF-8编码方案(前缀码)
```
0xxxxxxx (ASCII码)
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
```
**对字符的编码与解码**
```
- 对字符的编码
byte[] getBytes() 使用平台默认字符集编码
byte[] getBytes(String charsetName) 使用指定字符集编码为一系列字节。自己规定编码方式
- 对字符的解码
String(byte[] bytes) 提供平台默认字符集解码
string(byte[] bytes, String charsetName) 通过指定字符集解码
```
```
案例
String name = "我爱你中国abc666";
byte[] bytes = name.getBytes();
System.out.println(bytes.length); // 5*3+6 = 21
byte[] bytes2 = name.getBytes("GBK);
System.out.println(bytes.length); // 5*2+6 = 16
String name2 = new String(bytes, "GBK");
```
##### 5、IO流(字节流、字符流、缓冲流)
I指的是Input,称为输入流:负责把(磁盘、网络)数据读到内存中去。
O指的是Output,称为输出流,负责写数据出去。
**IO流分类**
按照流的方向
- 输入流
- 输出流
按照流的内容
- 字节流:适合操作所有类型的文件。
- 比如:音频、视频、图片、文本文件的复制转移等。
- 字符流:只适合操作纯文本文件。
- 比如:读写txt,java文件等。
**IO流的体系**
- 抽象类
- 实现类
- 字节输入流 `InputSream` 读字节数据的
- `FileInputStream`
- 字节输出流 `OutputStream` 写字节数据出去的
- `FileOutputStream`
- 字符输入流 `Reader` 读字符数据的
- `FileReader`
- 字符输出流 `Writer` 写字符数据出的
- `FileWriter`
##### 6、字节流
**FileInputStream(文件字节输入流)**
- 作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。
```
// 创建文件字节输入流管道于源文件接通
InputStream is = new FileInputStream(new File(pathname)); // 也可以直接写路径(推荐)
====================================================
// 每次读取文件中的一个字节,如果没有数据会返回-1。
int b;
while((b = is.read()) != -1){
操作b
}
// read() //每次读取一个字节,性能较差,读取汉字输出一定乱码。
// read(byte[] buffer) // 每次读取一个字节数组
====================================================
改造后:
byte[] buffer = new byte[n]; //每次读n个字节
int len; // 定义一个变量记住每次读取了多个字节
while ((len = is.read(buffer)) != -1){
String str = new String(buffer, 0, len);
}
// 每次读取多个自己,可以减少硬盘和内存的交互次数,从而提升性能。
// 缺陷:依然无法避免读取汉字输出乱码问题,存在截断汉字字节的可能性。
byte[] buffer = is.readAllBytes();// 可以定义一个与文件一样大的字节数组,一次性读完文件的全部字节。只适合读小文件。
```
**FileOutputStream文件字节输出流**
- 作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。
```
FileOutputStream(File file)
FileOutputStream(String pathname)
FileOutputStream(File file, boolean append) // 是否可扩展
FileOutputStream(String pathname, boolean)
OutputStream os = new FileOutputStream(String pathname);
write(int a) // 写一个字节
write(byte[] buffer) // 写一个字节数组
write(byte[] buffer, int pos, int len) // 片段
os.write(97);
os.write('b');
os.write("\r\n".getBytes()); // 换行
os.close() //不关闭会占据内存与硬盘之间的总线。需要关闭管道,释放管道占据的硬件资源。
```
**文件复制**
- 任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!
- 创建字节输入流管道read -> 字节数组 ->创建字节输出流管道write
- 字节流非常适合做文件的复制操作!
**资源释放问题**
```
try{
...
}catch(IOexception e){
e.printStackTrace();
}finally{
}
```
- finaly代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。
- 作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)
- 但是在资源释放时代码比较臃肿
JDK 7开始提供了更简单的资源释放方案:`try-with-resource`
```
try(定义资源1; 定义资源2; ...){
可能出现的异常代码;
}catch(异常类名 变量名){
异常的处理代码;
}
// 该资源使用完毕后,会自动调用其close()方法,完成对资源的释放。
try(InputStream fis = new FileInputStream(pathname1); OutputStream fos = new FileOutputStream(pathname2);){}catch(Exception e){}
```
- 注意
- ()中只能放置资源,否则报错
- 资源一般指的是最终实现类`AutoCloseable`接口。
##### 7、字符流
**FileReader文件字符输入流**
- 作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。
- 是按照字符读取,不会出现乱码!这是一种读取中文很好的方案。
**FileWriter文件字符输出流**
- 作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。
- 字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效。
```
public void flush() throws IOException //刷新流,就是将内存中缓存的数据立即写到文件中去生效。
public void close() throws IOException //关闭流的操作,包含了刷新。
```
为什么要刷新?
- 一开始每写一个字符就用IO写入字符,太多IO操作,性能降低。于是有了内存缓冲区,在内存缓冲区写完再进行一次IO,这样能提升性能。刷新就是通知内存把内存缓冲区的内容全部写到文件中。如果不刷新,内存缓冲区内的内容容易丢失。
- 刷新后,流可以继续使用。
##### 8、缓冲流
- 缓冲字节输入流:`BufferedInputStream`
- 缓冲字节输出流:`BufferedOutputStream`
- 缓冲字符输入流:`BufferedReader`
- 缓冲字符输出流:`BufferedWriter`
**缓冲字节流**
- 作用:可以提高字节输入流读取数据、和字节输出流写入数据的性能
- 原理:缓冲字节输入流自带8KB的缓冲池;缓冲字节输出流也自带8KB的缓冲池
```
// 使用方法
InputStream fis = new FileInputStream(srcpath);
InputStream bis = new BufferedInputStream(fis);
```
**缓冲字符流**
- 作用:自带8K的字符缓冲池,可以提高字符输入流读取字符数据的性能。缓冲字符输出流也一样。
- 使用方法:把低级的字符输入流包装成缓冲字符输入流管道,就可以直接使用。缓冲字符输出流也一样。`BufferReader(Reader r)`
- 新增功能:按照行读取字符
- `String readLine() //读取一行数据,如果没有数据返回null`
- 缓冲字符输出流新增功能:换行
- `void newLine()`
**常用的最优的读取文本的方案代码:不乱码,性能好**
```
String line = null;
while((line = rd.readLine()) != null){
操作line
}
```
##### 9、其他流
**字符输入转换流**
- `InputStreamReader` 字符输入转换流,继承自Reader。
- 当你代码是UTF-8,但是要读入GBK文件。(解决不同编码时,字符流读取文本内容乱码的问题。)
- 解决思路:先获取文件的原始字节流,再将其按真实的字符集编码成字符输入流,这样字符输入流中的字符就不乱码了。
- `public InputStreamReader(InputStream is, String charset)`
```
InputStream is = new FileInputStream(pathname); // GBK编码的文件
Reader isr = new InputStreamReader(is, "GBK"); // 指定字符集把原始字节流转换成字符输入流
BufferedReader br = new BufferedReader(isr); //创建缓冲字符输入流包装低级的字符输入流。
```
**打印流**
- `PrintStream` 继承字节输出流
- `PrintWriter` 继承字符输出流
- 作用:打印流可以更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
```
// 创建
PrintStream(OutputStream/File/文件地址)
printStream(文件地址, Charset charset)// 指定编码
printStram(OutputStream out, boolean autoFlush, String encoding) // 指定实现自动刷新,指定字符编码
void println() 打印任意类型数据出去
```
**特殊数据流**
用于通信
- `DataInputStream`
- `DataOutputStream` 允许把数据和类型一并写出去。
- 创建时候,可以包装基础的字节输出流。
```
writeByte(int v)
writeInt(int v)
writeDouble(Double v)
writeUTF(String str)
```
##### 10、IO框架
**什么是框架?**
- 框架(Framework)是一个预先写好的代码库或一组工具,旨在简化和加速开发过程。
- 框架的形式:一般是把类、接口等编译成class形式,再压缩成一个`.jar`结尾的文件发行出去。
- IO框架:封装了Java提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。
**步骤**
- 再项目中创建一个文件夹:lib
- 将commons-io-2.6.jar文件复制到lib文件夹
- 在jar文件上点右键,选择Add as Library -> 点击OK
- 在类中导包使用。
**使用**
```
FileUtils类提供的部分方法展示
void copyFile(File srcFile, File destFile) 复制文件
void copyDirectory() 复制文件夹
void deleteDirectory() 删除文件夹
String readFileToString(File file, String encoding) 读数据
void writeStringToFile(File file, String data, String charname, boolean append) 写数据
IOUtils类提供部分方法展示
int copy(InputStream i, OutputStream o) 复制文件
int copy(Reader r, Writer w)
write(String data, OutputStream o, String charsetName) 写数据
```
#### 7 多线程
- 线程(`Thread`) 是一个程序内部的一条执行流程
- 程序如果只有一条执行流程,那这个程序就是单线程的程序。
- 多线程:是指从软硬件上实现的多条执行流程的技术(多线程由CPU负责调度执行)。
**如何在程序中创建出多条线程?**
##### 1、多线程的创建方式一:继承`Thread` 类。
即将类声明为`Thread`的子类。此子类应覆盖`Thread` 的`run`方法。然后可以分配和启动子类的实例。
调用start方法启动一个新的线程执行;如果直接调用run方法会当成普通方法去执行,此时相当于还是单线程执行。
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
##### 2、多线程创建方式二:声明一个实现`Runnable` 接口的类。
该类然后实现 `run` 方法。然后可以分配类的实例,在创建Thread时作为参数传递,然后启动。
```
class PrimeRun implements Runnable{...重写run方法}
PrimeRun p = new PrimeRun();
new Thread(p).start();
```
- 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强。
- 缺点:需要多一个Runnable对象。
**匿名类写法**
- 可以创建Runnable的匿名内部类对象。
- 再交给Thread线程对象。
- 再调用线程对象的start()启动线程。
```
Thread t1 = new Thread(new Runnable(){重写run方法});
t1.start();
进一步简化
Thread t1 = new Thread(() -> {run方法内部});
t1.start();
```
##### 3、多线程创建方式三:实现Callable接口
前两种线程创建方式都存在的一个问题:
- 加入线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。
怎么解决这个问题?
- JDK 5.0 提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
- 这种方式最大的优点:可以返回线程执行完毕后的结果。
**步骤**
- 1、创建任务对象
- 定义一个类实现`Callable`接口,重写`call`方法,封装要做的事情,和要返回的数据。
- 把`Callable`类型的对象封装成`FutureTask`(线程任务对象)。
- 2、把线程任务对象交给`Thread` 对象。
- 3、调用`Thread` 对象的 `start` 方法启动线程。
- 4、线程执行完毕后、通过 `FutureTask` 对象的get方法去获取线程任务执行的结果。
**如果主线程发现第一个线程get时但是还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!**
```
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
public V get() throws Exception 获取线程执行call方法返回的结果。
```
```
class MyCallable implements Callable{
// 实现call方法。
public V call() throws Exception {
V v; // V是数据类型
return v;
}
}
=============================================
Callable c1 = new MyCallable();
FutureTask f1 = new FutureTask<>(c1)
// Runnable f1 = new FutureTask<>(c1)
// 未来任务对象:本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。可以获取线程执行完毕后的结果。
Thread t1 = new Thread(f1);
t1.start();
=============================================
// t1 -> f1 -> c1.call()
// 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
V v = f1.get(); //要处理异常,最好每一个线程都进行try-catch
```
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
- 缺点:编码复杂一点。
##### 4、三种线程的创建方式的比较:
- 继承`Thread` 类
- 优点:编程比较简单,可以直接使用Thread类中的方法
- 缺点:扩展性较差,不能再继承其他的类,不能返回线程执行的结果。
- 实现 `Runnable` 接口
- 优点:扩展性强,实现该接口的同时还可以继承其他的类。
- 缺点:编程相对复杂,不能返回线程执行的结果。
- 实现 `Callable` 接口
- 优点:扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果。
- 缺点:编程相对复杂。
```
public void run() 线程的任务方法
pubic void start() 启动线程
public String getName() 获取当前线程的名称,线程名称默认是 "Thread-索引"。(主线程名称是"main")
public void setName(String name) 为线程设置名称
public static Thread currentThread() 获取当前执行的线程对象。
public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()... 让调用当前这个方法的线程先执行完!线程插队。
```
```
Thread提供的常见构造器
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象称为线程对象
public Thread(Runnable target, String name) 封装Runnable对象称为线程对象,并指定线程名称。
```
**Thread的其他方法说明**
Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用。
##### 5、线程安全和线程同步
**01 什么是线程安全?**
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
**02 线程同步**
线程同步是线程安全问题的解决方案。
- 方式一:同步代码块
- 方式二:同步方法
- 方式三:lock锁
**03 线程同步的核心思想**
- 让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。
**04 线程同步的常见方案**
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
**05 方案一:同步代码块**
- 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
- `synchronized(同步锁) {访问共享资源的核心代码}`
- **原理**:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
- 同步锁的**注意事项**:
- 对于当前同时执行的线程来说,**同步锁必须是同一把(同一个对象)**,否则会出bug。
- 那锁对象随便选择一个唯一的对象好不好呢?
- 不好,会影响其他无关进程
- 建议使用共享资源作为锁对象,对于**实例方法**建议使用 `this` 作为锁对象。
- 对于**静态方法**建议使用字节码(`类名.class`)对象作为锁对象。
**06 方案二:同步方法**
- 作用:把访问共享资源的核心方法给上锁,以保证线程安全。
- `修饰符 synchronized 返回值类型 方法名称(形参列表){操作共享资源的代码}`
- **原理**:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
- 同步方法底层原理:
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是**实例方法**:同步方法默认用`this` 作为的锁对象。
- 对于**静态方法**建议使用字节码(`类名.class`)对象作为锁对象。
**07 同步代码块还是同步方法好?**
- 范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
- 可读性:同步方法更好。
**08 方案三:Lock锁**
- Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类`ReentrantLock` 来构建 `Lock`锁对象
- `public ReentrantLock() 获得Lock锁的实现类对象`
- Lock的常用方法:
- `lock()` 获得锁
- `unlock()` 释放锁
- `boolean tryLock()` 只有在调用时它是空闲的才能获取锁。
```
private final Lock lk = new ReentrantLock(); //最好加final,这样不会被人修改。保护锁对象。
lk.lock();
try{}
else{}
finally{lk.unlock();}
// 在finally内解锁。确保锁用完了一定会被释放。
```
##### 6、线程池ExecutorService
- 线程池就是一个可以复用线程的技术。(重复利用线程)
**不使用线程池的问题**
- 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程去处理的,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
- 工作线程`WorkThread` (可以拿来工作的线程,有限的)
- 任务队列 `WorkQueue`
- 任务接口只能是 `Runnable` 和 `Callable`
**如何创建线程池对象?**
- 方式一:使用 `ExecutorService` 的实现类 `ThreadPoolExecutor` 自创建一个线程池对象。
- 方式二:使用`Executor` (线程池的工具类)调用方法返回不同特点的线程池对象。
##### 7、通过ThreadPoolExecutor创建线程池
```
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) //使用指定的初始化参数创建一个新的线程池对象。
参数一:corePoolSize:指定线程池的核心线程的数量。(正式工:3)
参数二:maximumPoolSize:指定线程池的最大线程数量。(最大员工数:5)-(临时工:2)
参数三:keepAliveTime:指定临时线程的存活时间。(临时工空闲多久被开除)
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五:workQueue:指定线程池的任务队列。(客人排队的地方)
参数六:threadFactory:指定线程池的线程工厂。(负责招聘员工的hr)
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理。)(忙不过来咋办?)
```
**ExecutorService的常用方法**
```
void execute(Runnable command) 执行Runnable任务
Future submit(Callable task) 执行Callable任务,返回未来任务对象,用于获取线程返回的结果。
void shutdown() 等全部任务执行完毕后,再关闭线程池!// 一般不关线程池。
List shutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务。
```
```
ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
pool.execute(new MyRunnable());
Future f = pool.submit(new MyCallable());
System.out.println(f.get());
```
**什么时候开始创建临时线程?**
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
**什么时候会拒绝新任务?**
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
**任务拒绝策略**
```
ThreadPoolExecutor.AbortPolicy() 丢弃任务并抛出RejectedExecutionException异常。 -- 是默认的策略。
ThreadPoolExecutor.DiscardPolicy() 丢弃任务,但是不抛出异常,这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy() 抛弃队列中等待最久的任务,然后把当前任务加入到队列中。
ThreadPoolExecutor.CallerRunsPolicy() 由主线程负责调用任务的run()方法从而绕过线程池直接执行。
```
##### 8、通过Executors创建线程池
- 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
```
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程池数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
```
- 这些方法的底层,都是通过线程池的实现类`ThreadPoolExecutor` 创建的线程池对象。
**Executors使用可能存在的陷阱**
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。(资源耗尽)
- 说明:
- `FixedThreadPool` 和 `SingleThreadPool` 允许的请求队列长度为 `Integer.MAX_VALUE` ,可能会堆积大量的请求,从而导致`OOM` (内存溢出)
- `CachedThreadPool` 和 `ScheduledThreadPool` 允许创建线程数量为 `Integer.MAX_VALUE` ,可能会创建大量的线程,从而导致`OOM` (内存溢出)
##### 9、理解并发和并行
进程:
- 正在运行的程序(软件)就是一个独立的进程。
- 线程是属于进程的,一个进程中可以同步运行很多个线程。
- 进程中的多个线程其实是并发和并行执行的。
并发:
- 进程中的线程是由CPU负责调度执行的,但是CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为每个系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程是在同时执行,这就是并发。
并行:
- 在同一时刻上,同时有多个线程在被CPU调度执行。
并发和并行是同时进行的。
#### 8 网络编程
- 可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)。
**基本的通信架构**
- 基本的通信架构有2种形式:CS架构(`Client 客户端/Server服务端` )、BS架构(`Browser浏览器/Server服务器` )。
- `Client-Server(CS)` 架构
- `Client客户端` :需要程序员开发客户端软件,需要用户下载安装客户端软件。
- `Server服务端` :需要程序员开发服务端程序。
- `Browser-Server(BS)` 架构
- `Browser浏览器` :不需要程序员开发。
- `Server服务端` :需要程序员开发服务端程序。
- 无论是CS架构,还是BS架构的软件都必须依赖网络编程!
- `java.net.*` 包下提供了网络编程的解决方案!
##### 1、网络编程三要素:IP、端口、协议
- 网络通信三要素
- IP地址:设备在网络中的地址,是设备在网络种的唯一标识。
- 端口:应用程序在设备中的唯一标识。
- 协议:连接和数据在网络种传输的规则。
##### 2、IP地址
- IP地址:
- ipv4(32位)、ipv6(64位),ipv6兼容ipv4.
- IP域名:识别和定位网站的人类可读的名称。
- DNS域名解析:
- 是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的“电话簿”,将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
- 公网IP、内网IP
- 公网IP:可以连接到互联网的IP地址。
- 内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址。
- 本机IP
- 127.0.0.1、localhost:代表本机IP,只会寻找当前程序所在的主机。
**InetAddress** 的常用方法
```
public static InetAddress getLocalHost() throws UnknownHostException 获取本机IP,返回一个InetAddress对象。
public String getHostName() 获取ip地址对应主机名
public String getHostAddress() 获取ip对象中ip地址信息
public static InetAddress getByName(String host) throws UnknownHostException 根据ip地址或者域名,返回一个InetAddress对象。
public boolean isReachabnle(int timeout) throws IOException 判断主机在指定毫秒内与该ip对应的主机能否连通?
```
##### 3、端口
- 用来标记正在计算机设备上运行的应用程序,被规定位一个 16 位的二进制,范围是
- 端口分类
- 周知端口:0-1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
- 注册端口:1024-49151,分配给用户进程或某些应用程序
- 动态端口:49152-65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
- 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。
##### 4、通信协议
- 网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
- 开放式网络互联标准:OSI网络参考模型。
- OSI网络参考模型:全球网络互联标准。
- TCP/IP网络模型:事实上的国际标准。
- 数据链路层+物理层(比特流)
- 网络层(IP)- 封装源和目标IP
- 传输层(UDP、TCP)
- 应用层(HTTP、FTP、SMTP...)
- UDP协议:
- 特点:无连接、不可靠通信。(速度快)
- 不事先建立连接,数据按照包发,一般数据包含:自己的IP、端口、目的地IP端口和数据(限制在64KB内)等。
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。(常用于通话等)
- TCP协议:
- 特点:面向连接,可靠通信。
- TCP的最终目的:要保证在不可靠的信道上实现可靠的数据传输。
- TCP主要有三个步骤实现可靠传输:**三次握手建立连接,传输数据进行确认,四次挥手断开连接。**
- 常用于网页、文件下载、支付等。。效率不高但是安全。
- 三次握手建立可靠连接
- 可靠连接:确保通信的双方收发消息都是没问题的(全双工)
- 发出连接请求 -> 返回一个响应 -> 再次发出确认信息,连接建立。
- 传输数据会进行确认,以保证数据传输的可靠性。
- 四次挥手断开连接
- 目的:确保通信的双方收发消息都已经完成。
- 发出断开连接请求 -> 返回一个响应:稍等 -> 返回一个响应:消息处理完毕,确认断开 -> 发出确认断开信息,连接断开。
##### 5、UDP通信的实现
- Java提供了一个 `java.net.DatagramSocket` 类来实现UDP通信。
```
DatagramSocket:用于创建客户端、服务端
public DatagramSocket() 创建客户端的Socket对象,系统会随机分配一个端口号。
public DatagramSocket(int port) 创建服务端的Socket对象,并指定端口号
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 使用数据包接收数据。
DatagramPacket: 创建数据包
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发出去的数据包对象。
// 参数一:发送的数据,字节数组
// 参数二:发送的字节长度
// 参数三:目的地的IP地址
// 参数四:服务端程序的端口号
public DatagramPacket(byte[] buf, int length) 创建用来接收数据的数据包。
public int getLength() 获取数据包,实际接收到的字节个数。
```
##### 6、TCP通信的实现
- Java提供了一个 `java.net.Socket` 类来实现UDP通信。
- 客户端是通过 `java.net` 包下的Socket类来实现的
```
public Socket(String host, int post) 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端Socket
方法
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象
```
- 服务端是通过 `java.net` 包下的 `ServerSocket` 类来实现
```
public ServerSocket(int port) 为服务端程序注册端口
方法
public Socket accept() 阻塞等待客户端连接,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。
```
##### 7、TCP同时接收多个客户端的消息(UDP可以)
- 目前我们开发的服务端程序,是否可以支持同时与多个客户端通信?
- 不可以
- 因为服务端现在只有一个主线程,只能处理一个客户端的消息。
- 该如何支持与多个客户端同时通信呢?
- 引入多线程
- 主线程定义了循环负责接收客户端Socket管道连接。
- 每收到一个Socket通信管道后分配一个独立的线程负责处理它。
##### 8、B/S架构的原理
```
http://服务器IP:服务器端口
比如: http://127.0.0.1:8080
```
注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。
```
--------------------------------------------------
协议版本 空格 状态码 空格 状态符 回车换行
头部字段名:值; 回车换行
...... 回车换行
头部字段名:值; 回车换行
必须单独换一行
响应正文(真正给浏览器展示的网页数据)
--------------------------------------------------
HTTP/1.1 200 OK \r\n
Content-Type:text/html; charset=UTF-8 \r\n
...... \r\n
...... \r\n
\r\n
...
--------------------------------------------------
```
**拓展知识**
- 每次请求都开一个新线程,到底好不好?
- 高并发时,容易宕机!
- 使用**线程池**进行优化!
#### 9 Java高级技术(Junit、反射、注解、动态代理)
源码、框架、架构师层面的技术。
##### 1、单元测试
- 就是针对最小的功能单位:方法,编写测试代码对其进行正确性测试。
之前是如何进行单元测试的?有啥问题?
- 只能在main方法种编写测试代码,去调用其他方法进行测试。
- 无法实现自动化测试,一个方法测试失效,可能影响其他方法的测试。
- 无法得到测试的报告,需要程序员自己去观察测试是否成功。
**Junit单元测试框架**
- 可以用来对方法进行测试,它是第三方公司开源出来的(很多开发工具已经集成了Junit框架,比如IDEA)
- 优点:
- 可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
- 不需要程序员去分析测试的结果,会自动生成测试报告出来。
- 步骤:
- (1)将Junit框架的jar包导入到项目中(IDEA已经自动集成)。
- (2)为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
- (3)测试方法上必须声明`@Test` 注解,然后在测试方法中,编写代码调用被测试的业务方法进行测试。
- (4)开始测试:如果测试通过为绿色、测试失效为红色。
##### 2、反射
反射就是:加载类,并允许以编程的方式访问有关已加载类的字段,方法和构造函数的信息,以及使用反射字段,方法和构造函数在封装和安全限制内对其底层对应项进行操作。
**获取类的信息、操作它们**
- 反射第一步:加载类,获取类的字节码:class对象
- `Class c1 = 类名.class`
- 调用Class提供的方法:`public static Class forNmae(String package);
- Object提供的方法:`public Class getClass(); Class c3 = 对象.getClass();
- 获取类的构造器:Constructor对象
- 获取类的成员变量:Field对象
- 获取类的成员方法:Method对象
```
public class A{}
Class a = A.class;
// 构造器
Constructor[] cons = a.getConstructors(); // public修饰
// Constructor[] cons = a.getDeclaredConstructors(); // 所有
Constructor con1 = a.getConstructor(String.class, int.class) //2个变量的单个构造器,
// 成员变量
Field[] fields = a.getField() // a.getDeclaredFields()
// 方法
Method[] methods = a.getMethods(); // a.getDeclaredMethods()
```
**使用**
- (1)获取构造器的作用依然是创建对象。
- `A a = (A)con.newInstance();`
- 可以暴力反射:暴力反射可以访问私有的构造器、方法、属性。
- `con.setAccessible(true); // 绕过访问权限,直接访问!`
- 获取成员变量的作用依然是赋值、取值
- `void set(Object obj, Object value); // 赋值`
- `Object get(Object obj); // 取值`
- `public void setAccessible(boolean flag); // 设置为true,表示禁止检查访问控制:暴力反射。`
- 获取成员方法的目的依然是调用方法。
- `public Object invoke(Object obj, Object...args) 触发某个对象的该方法执行。`
- `public void setAccessible(boolean flag); // 设置为true,表示禁止检查访问控制:暴力反射。`
**作用和应用场景**
- 基本作用:可以得到一个类的全部成分然后操作。
- 可以破坏封装性。
- 可以绕过泛型的约束。(获取ArrayList的类对象,获取add方法,然后直接添加数据)
- 最重要的用途是:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。
- 比如:对于任意类,获取该类的全部成员变量和成员变量的值存到文件中,就通过反射来做。
##### 3、注解(重点)
- 注解就是Java代码里的特殊标记,比如:@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行该程序。
- 注意:注解可以用在类上、构造器上、方法上、成员变量上、参数上、等位置上。
**自定义注解**
```
public @interface 注解名称{
public 属性类型 属性名() default 默认值;
}
```
自定义注解的特殊属性名:value
- 如果注解中只有一个value属性,使用该注解时,value名称可以不写!
- `@A(value="delete") 可以写成 @A("delete")`
- 如果有其他属性,但是有默认值也可以不写。
**原理**
- 注解本质是一个接口,Java中所有注解都是继承了 `Annotation` 接口的。
- `@注解(...)` :其实就是一个实现类对象,实现了该注解以及`Annotation` 接口。
##### 4、元注解
- 指的是:拿来注解 注解 的注解
```
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test{}
```
```
@Target(ElementType.x1, ...) 声明被修饰的注解只能在哪些位置使用
1 TYPE 类、接口
2 FIELD 成员变量
3 METHOD 成员方法
4 PARAMETER 方法参数
5 CONSTRUCTOR 构造器
6 LOCAL_VARIABLE 局部变量
```
```
@Retention(RetentionPolicy.x1, ...) 声明注解的保留周期
1 SOURCE 只作用在源码阶段,字节码文件中不存在
2 CLASS(默认值) 保留到字节码文件阶段,运行阶段不存在
3 RUNTIME(开发常用) 一直保留到运行阶段
```
##### 5、注解的解析和应用场景
- 就是判断类上、方法上、成员变量上是否存在注解,并把注解里的内容给解析出来。
如何解析注解?
- 指导思想:要解析谁上面的注解,就应该先拿到谁
- 比如要解析类上面的注解,先获取该类的Class对象,再通过Class对象解析其上面的注解。
- 比如要解析成员方法上的注解,则应该获取到该Method对象,再通过Method对象解析其上面的注解。
- Class、Method、Field、Constructor都实现了 `AnnotatedElement` 接口,它们都拥有解析注解的能力。
```
public Annotation[] getDeclaredAnnotations() 获取当前对象上面的注解
public T getDeclaredAnnotation(Class annotationClass) 获取指定对象
public boolean isAnnotationPresent(Class annotationClass) 判断当前对象上是否存在某个注解
```
**应用场景**
- 比如:使用注解可以开发出一个简易版的Junit框架
##### 6、动态代理
- 程序为什么需要代理?代理长什么样?
- 对象如果嫌身上干的事太多,可以通过代理来转移部分职责。
- 对象有什么方法想被代理,代理就一定要有对应的方法
- 被代理对象设置为接口。
- `java.lang.reflect.Proxy`类:提供了为对象产生代理对象的方法:
```
public static Object newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler h)
参数一:指定用哪个类加载器,去加载生成的代理类
参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
参数三:指定生成的代理对象要干什么事情
```
- spring就是一个代理工厂。对象 -> spring -> 代理对象