1. Java入门
1.1 基本概念
JDK:Java开发工具包,包含JRE+开发工具
JRE:Java运行环境,包含JVM+Java核心类库
JVM:Java虚拟机
1.2 环境变量配置
JAVA_HOME环境变量:它指向JDK的安装目录Eclipse/NetBeans/Tomcat等软件就是通过搜索JAVA_HOME变量来找到并使用安装好的jdk。
Path环境变量:作用是指定命令搜索路径,在cdm下面执行命令时,它会到PATH变量所指定的路径中查找看是否能找到相应的命令程序。我们需要把 jdk安装目录下的bin目录增加到现有的PATH变量中,bin目录中包含经常要用到的可执行文件如javac/java/javadoc等,设置好PATH变量后,就可以在任何目录下执行javac/java等工具了。
操作:在系统变量中增加JAVA_HOME变量,值是JDK的安装目录,然后在Path中新建%JAVA_HOME%\bin
2. 数据类型
2.1 数据类型分类
基本数据类型(静态,非静态,局部常量/变量都有):
整型:byte(1,-128~127) short(2) int(4) long(8)
浮点型:float(4) double(8)
字符型:char(2)
布尔型:boolean(1)
引用数据类型:
数组,类,接口,枚举
整形字面值默认int,浮点字面值默认double
2.2 基本数据类型转换
数据转换规则:从低位类型到高位类型自动转换,从高位类型到低位类型需要强制类型转换
算术运算中的类型转换:先转换为高位数据类型,再参加运算,结果也是最高位的数据类型.
- 如操作数之一为double,则另一个操作数先被转化为double,再参与算术运算
- 如两操作数均不为double,当操作数之一为float,则另一操作数先被转换为float,再参与运算
- 如两操作数均不为double或float,当操作数之一为long,则另一操作数先被转换为long,再参与算术运算
- 如两操作数均不为double,float,long,则两操作数先被转换为int,再参与运算
- 如采用+=,*=,++,–等缩略形式的运算符,系统会自动强制将运算结果转换为目标变量的类型
- byte short char之间运算会转换为int
- (byte short) (char)这两组相互之间不会自动转换
2.3 基本数据类型和String之间转换
基本类型转换为String:
方式一:+连接
方式二:包装类对象.toString()
方式三:String.valueOf()
String转换成对应的基本类型:
除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:
public static int parseInt(String s)
3. 运算符
(算术,比较,逻辑,赋值,三目,位) ->表达式
3.1 算术运算符
重点:
i++:先自增,然后返回自增之前的值
++i:先自增,然后返回自增之后的值
3.2 比较运算符
== != < <= > >= instanceof(判断运行类型是否是类或子类的对象)
3.3 逻辑运算符
1.短路与&&,短路或||,取反!
2.逻辑与&,逻辑或|,逻辑异或^(不同为true)
对于&&短路与而言,如果第一个条件为 false ,后面的条件不再判断,效率高
3.4 三目运算符
条件表达式 ? 表达式1 : 表达式2;
注意:看做一个整体,结果为最大的类型
3.5 位运算符
3.5.1 进制
二进制:0b或0B开头
八进制:0开头
十进制
十六进制:0x或0X开头
3.5.2 原码,反码,补码
二进制最高位0代表正数,1代表负数
正数的原码,反码,补码都一样
负数的反码=原码符号位不变,其他位取反
负数的补码=反码+1
计算机运算用补码,看结果要看原码
3.5.3 7个位运算符
&(全为1结果为1) | ^(相同为0不同为1) ~
>>:算术右移,低位溢出,符号位不变,并用符号位补溢出的高位,本质/2
<<:算术左移,符号位不变,低位补0,本质*2
>>>:无符号右移,低位溢出,高位补0
3.6 保留字
现有 Java 版本尚未使用,但以后版本可能会作为关键字使用,自己命名标识符时要避免使用这些保留字:
byValue cast future generic inner operator outer rest var goto const
3.7 标识符命名规则
1.由26个英文字母大小写,0-9,_,$组成
2.不可以用数字开头
3.不可以使用关键字和保留字,但能包含关键字和保留字
4 流程控制
4.1 顺序控制
4.2 分支控制
if…else…
switch case break default
注意:switch可以作用于char byte short int及他们的包装类型,不可以作用于long float boolean及他们的包装类型
4.3 循环控制
for
while
do while
4.4 跳转语句
break continue
5 数组
一维数组:
动态初始化1: int[] arr = new int[3] 拥有默认值
动态初始化2: int[] arr; arr = new int[3] 先声明,再分配空间
静态初始化1: int[] arr = {1, 2, 3}
静态初始化2: int[] arr=new int[]{1,2,3}
注意:int[] a; a = {1,2,3,4,5};这样是不行的
二维数组:
动态初始化1: int[][] arr = new int [3][5] 用来存储3个一维数组,每个数组存5个元素
动态初始化2: int[][] arr = new int [3][] 有三个一维数组,但每个一维数组没有开辟空间
静态初始化: int[][] arr={{22,15,32,20,18},{12,21,25,19,33},{14,58,34,24,66}};
数组创建后,如果没有赋值,默认值如下
short 0, byte 0, int 0, long 0, float 0.0, double 0.0, char \u0000, boolean false, Object null
获取数组长度 数组名称.length
5.1 冒泡排序
public class BubbleSort{
public static void BubbleSort(int[] arr) {
int temp;
for(int i = 0; i < arr.length - 1; i++) {
for(int j = 0; j < arr.length - i - 1, j++) {
if(arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
优化:
public class BubbleSort {
public static void BubbleSort(int[] arr) {
boolean flag;//判断数组是否有序
int temp;
for (int i = 0; i < arr.length - 1; i++) {
flag = true;//每轮比完后都假设有序
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false;//如果进入了交换,认为无序
}
}
if (flag) {
break;//如果这一轮没有发生交换,说明有序,就不用比下一轮了
}
}
}
}
6 面向对象基础
6.1 类与对象的关系
类:一类事物的模板,具有属性,方法,构造器,代码块,内部类五大成员
对象:类的一个具体的实例
6.2 JVM内存划分
虚拟机栈:方法在栈中运行,存放方法中的局部变量
堆:对象,数组,new出来的
方法区:类信息(属性,方法),常量池
本地方法栈
程序计数器
类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区
堆当中的方法是一个地址值,指向方法区
6.3 方法重载
方法名称相同,参数列表(个数,数据类型,顺序)不同,与修饰符和返回值类型无关
6.4 可变参数
一个方法需要接受多个参数,并且多个参数类型一致,本质就是数组
格式:修饰符 返回值类型 方法名(参数类型… 形参名){ }
注意:如果在方法书写时,这个方法拥有多个参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置
6.5 成员变量和局部变量
成员变量可以不赋值,局部变量声明后必须赋值才能使用
在类中位置不同(类中,方法中),作用范围不同(可以被本类或其他类使用,只能在方法中使用),初始化值不同(有,无),内存中位置不同(堆,栈),生命周期不同(成员变量伴随着对象的创建而创建,伴随着对象的销毁而销毁,局部变量随着代码块的执行而创建,代码块结束就销毁),修饰符不同(可以有,无)
6.6 构造器
方法名和类名相同,没有返回值,在创建对象时,系统会自动的调用该类的构造器完成对象的初始化
如果没有定义构造器,赠送一个无参的,一旦定义了构造器,会被覆盖
6.7 对象创建的流程
1.首先加载从子类到父类加载类信息(只加载一次),执行顺序为父类的静态属性默认初始化,显式初始化,静态代码块,静态方法声明,子类静态属性默认初始化,显式初始化,静态代码块,静态方法声明(静态代码块和静态属性显式初始化优先级一样)
2.接下来在堆中分配空间创建对象,执行顺序为父类普通属性默认初始化,显式初始化,构造代码块,普通方法的声明,父类构造方法,接下来调用子类子类普通属性默认初始化,显式初始化,构造代码块,普通方法的声明,子类构造方法(构造代码块和普通属性优先级一样)
3.返回对象在堆中的引用
6.8 this,super关键字
this代表所在类的当前对象的引用,可以用来访问本类的成员变量、方法、构造器
作用1:当本类的成员变量与局部变量同名时,指定本类的成员变量,规避就近原则
作用2:调用构造方法,主动调用构造就是通过this/super完成的
super代表父类的引用,可以在子类中访问父类的属性,方法,构造器,但不能访问private的
作用1:用super()调用构造器可以使父类属性由父类初始化,子类属性由子类初始化
作用2:当子类中有和父类的成员重名时,为了访问父类的成员,必须使用super,如果没有重名,使用super,this,直接访问是一样的效果
6.9 访问修饰符
不同权限的访问权力(可以修饰类,方法,属性,但是修饰类只能用public和默认)
范围从小到大 public protected default private
同一类中 √ √ √ √
同一包中 √ √ √ ×
不同包的子类 √ √ × ×
不同包中的无关类 √ × × ×
建议:
成员变量使用 private,隐藏细节
构造方法使用 public,方便创建对象
成员方法使用 public,方便调用方法
记忆:default访问特性是包访问性,protected扩大一级到子类
7 面向对象三大特征
7.1 封装
隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,增强代码的的可维护性,同时也保护了数据
1.使用private关键字来修饰成员变量,只有在本类中才能访问
2.对需要访问的成员变量,提供对应的一对getXxx 方法,setXxx 方法
7.2 继承
1.子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为,子类可以直接访问父类中的非私有的属性和行为.
2.当子类创建好之后,建立查找的关系,访问方法/属性,首先看子类有没有,没有则一级一级向上找父类
3.提高了代码的复用性和可维护性
7.2.1 方法重写
子类中出现与父类一样的方法时(方法名和参数列表都相同),会出现覆盖效果,声明不变,重新实现@Override
要遵循“两同两小一大”
“两同”:方法名相同、形参列表相同
“两小”:子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
“一大”:子类方法的访问权限应比父类方法的访问权限更大或相等
7.2.2 继承中的构造方法
理解:因为构造方法名字和类名一样,所以子类无法继承,但是构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作
特点:子类的构造方法中默认有一个super() ,表示调用父类的构造方法,是子类构造方法的第一句且只有一个.super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
7.3 多态
方法或对象具有多种形态,建立在封装和继承基础之上
体现:
1.方法的重写/重载(方法多态)
2.对象的编译类型和运行类型可以不一致(对象多态)
7.3.1 对象的多态
7.3.1.1 向上转型
格式:
父类类型 变量名 = new 子类对象;
变量名.方法名();
Fu f = new Zi();
f.method();
特点:
1.编译类型看等号左边,可以变化
2.运行类型看等号右边,一旦定义,不能改变
3.可以调用父类所有成员(遵守访问权限),不能调用子类特有成员
7.3.1.2 向下转型
格式:
子类类型 变量名 = (子类类型) 父类变量名;
Zi zi =(Zi) f;
特点:
1.向下转型后,就可以调用子类所有的成员变量名
2.转换前可以使用 变量名 instanceof 数据类型 判断对象的运行类型是否是XX类型或XX类型的子类型
7.3.2 动态绑定机制
1.当调用对象方法的时候,该方法会和对象的内存地址/运行类型绑定
2.当调用对象属性的时候,没有动态绑定机制,哪里声明,哪里使用
7.3.3 多态的作用
1.多态数组,即数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
2.父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用
3.父类类型作为方法返回值参数,返回子类对象
8 面向对象高级
8.1 静态变量和静态方法
static关键字 用来修饰的成员变量和成员方法,被修饰的成员是属于类的
成员变量 对象名调用
成员方法 对象名调用
静态变量 对象名类名都可以调用,推荐类名(编译器会自动转换),本类中可以省略
静态方法 对象名类名都可以调用,推荐类名,本类中可以省略
注意:
1.静态不能直接访问非静态,内存中先有静态(方法中的静态区)后有非静态
2.可以通过一个对象的引用传入静态方法中,再去调用该对象的非静态方法
3.静态方法不能用this,因为与方法无关
8.1.1 静态成员能否被继承
静态属性和静态方法可以被继承,静态方法不可以被重写,不能实现多态
8.2 代码块
静态代码块:
public class 类名称 {
static {
// 当第一次用到本类时,静态代码块执行唯一的一次。可以用来一次性地对静态成员变量进行赋值。
// 加载需要在第一时间就加载且只加载一次的资源。
}
}
构造代码块:
{
//方法外,在每个对象生成时都会被执行一次,可以初始化类的实例变量。提取所有构造方法的共性。
}
局部代码块:
{
//方法内,控制变量的作用范围
}
执行顺序:静态代码块—>非静态代码块—>构造函数
8.3 单例模式
确保一个类只有一个实例
饿汉式实现方式
public class Runtime {
//1.创建静态的全局唯一的对象
private static Runtime currentRuntime = new Runtime();
//2.私有化构造方法,不让外部来调用
private Runtime() {}
//3.通过自定义的静态方法获取实例
public static Runtime getRuntime() {
return currentRuntime;
}
}
懒汉式实现方法
public class Runtime {
//1.先创建,延迟加载
private static Runtime currentRuntime;
//2.私有化构造方法,不让外部来调用
private Runtime() {}
//3.通过自定义的静态方法获取实例
public static Runtime getRuntime() {
if (currentRuntime == null) {
currentRuntime = new Runtime();//没有对象时才创建对象
}
return currentRuntime;
}
}
思路:
1.对本类构造方法私有化,防止外部调用构造方法创建对象
2.创建全局唯一的对象,也做私有化处理
3.通过自定义的公共方法将创建好的对象返回
8.4 final关键字
类:被final修饰的类,不能被继承。
方法:被final修饰的方法,不能被重写。
变量:被final修饰的变量,不能被重新赋值。
final和static同时修饰的常量,使用时不会导致类加载
8.5 抽象
由来:
抽象父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。
那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。
抽象类是后天向上抽取共性形成的结果
抽象类:
指包含抽象方法的类,但还是一个类,可以有任意成员
如果一个类包含抽象方法,那么该类必须是抽象类
抽象类不能被实例化
abstract class 类名字 { }
抽象方法:
不能使用private,final,static(这些关键字与重写相违背)来修饰
抽象方法不能有方法体
修饰符 abstract 返回值类型 方法名 (参数列表);
抽象的使用:
继承抽象类的子类必须重写(实现)父类所有的抽象方法。否则,该子类也必须声明为抽象类。
最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。
8.6 接口
概念:更加抽象的抽象类,方法的集合.是预先定义好的规则
包含:抽象方法(JDK7及以前),默认方法和静态方法(JDK8)
定义:
public interface 接口名称 {
// 抽象方法:使用 abstract 关键字修饰,可以省略,没有方法体。该方法供子类实现使用。
// 默认方法:使用 default 修饰,不可省略,供子类实现后调用或者子类重写。
// 静态方法:使用 static 修饰,供接口直接调用(不可以通过实现类的类名或者实现类的对象调用)
}
子类实现接口:
1.必须重写接口中所有抽象方法。
2.继承了接口的默认方法,即可以直接调用,也可以重写。
class 类名 implements 接口名 {
// 重写接口中抽象方法(必须)
// 重写接口中默认方法(不重名时可选)
}
接口与接口的关系:
是继承关系,可以单继承,也可以多继承
interface A extends B,C{}
特点:
接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
接口中,没有构造方法,不能创建对象
接口中,没有代码块
8.1.1 实现接口VS继承类
一个类只能继承一个类但可以实现多个接口,继承的主要价值在于解决代码的复用性和维护性,接口的主要价值在于设计好各种方法,让其他类去实现,更加灵活.
8.7 内部类
如果定义类在局部位置(方法中/代码块),可以分为局部内部类/匿名内部类
如果定义在成员位置,可以分为成员内部类/静态内部类
8.7.1 局部内部类
定义在类中方法内/代码块内的类,地位是一个局部变量,但本质还是一个类
不能加访问修饰符,可以加final(局部变量的性质)
访问:
可以访问外部类的成员,包括私有成员,重名时,使用外部类名.this.成员
外部类要访问内部类的成员,需要在内部类所在方法中创建局部内部类对象
外部其他类不能访问局部内部类,因为地位相当于一个局部变量
class Outer{
private int a = 1;
public void method() {
class Inner{
private int a = 2;
a = Outer.this.a;
}
}
}
8.7.2 匿名内部类(重点)
整个是一个带具体实现的父类或者父接口的匿名的子类对象。大括号里面的部分是匿名内部类
下面可以直接当匿名对象使用,也可以赋给左边新建的父类/接口类型引用
匿名内部类是省略了实现类/子类名称,但是匿名对象是省略了对象名称
A a = new A() {
@Override
public void method() {
System.out.println("重写A类的方法");
}
};
8.7.3 成员内部类
定义在类中方法外的类,即成员位置,可以加任意访问修饰符
访问:
可以访问外部类的成员,包括私有成员,重名时,使用外部类名.this.成员
外部类要访问内部类的成员,必须要建立内部类的对象。
在外部其他类创建内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
Outer.Inner obj = new Outer().new Inner();
class Outer{
public class Inner{
//执行语句
}
}
8.7.4 静态内部类
定义在成员位置且有static修饰
访问:
可以访问静态,不能访问非静态,重名时,使用外部类名.成员
外部类要访问静态内部类的成员,必须要建立静态内部类的对象。
在外部其他类创建静态内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().内部类型();
Outer.Inner obj = new Outer().Inner();
class Outer{
public static class Inner{
//执行语句
}
}
8.8 各类修饰符总结
8.8.1类修饰符
访问控制修饰符:public default(注意:成员/静态内部类因为相当于一个成员,可以加任意访问修饰符)
非访问控制修饰符:abstract final
8.8.2 方法修饰符
访问控制修饰符:public prtected default(不写) private
非访问控制修饰符:abstract static final native synchronized
8.8.3 变量修饰符
访问控制修饰符:public protected default(不写) private
非访问控制修饰符:static final volatile transient
9 常用类
9.1 Object类
9.1.1 ==和equals的区别
==作为一个运算符,既可以用来判断基本类型的值的是否相等,也可以判断引用类型的地址是否相等
equals是Object类中的一个方法,原生用于判断引用类型的地址是否相等,通常会在子类中重写这个方法来比较对象的类型和属性是否相等
9.1.2 重写equals方法
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()) {
return false;
}
User u = (User)o;
return (id == u.id) || ((id != null) && id.equals(u.id)) && Double.doubleToLongBits(d) == Double.doubleToLongBits(u.d) &&Float.floatToIntBits(f) == Float.floatToIntBits(u.f);
9.1.3 重写hashCode方法
返回根据内存地址得出的32位十进制哈希码值,此方法是为了在具有哈希表的容器中使用
按照equals( )中比较两个对象是否一致的条件用到的属性来重写hashCode(),例:
public int hashCode() {
int result = 1;
result = 31 * result + (id == null ? 0 :id.hashCode());
result = 31 * result + (name == null ? 0 : name.hashCode());
return result;
}
为什么用31:
1.31 是一个奇质数,减少hash碰撞,如果过大可能溢出
2.2个5次方是32,那么也就是 31 * i == (i << 5) - i,乘积运算可以使用位移提升性能
9.1.4 equals方法和hashCode方法的关系
如果两个对象使用equals比较返回true,那么它们的hashCode值一定要相同。
如果返回false,那么它们的hashCode值不一定要相同。
重写equals时必须重写hashCode方法,目的是为了保证HashSet/HashMap/Hashtable类等元素不重复
他们都是根据存储对象的hashcode值来进行判断是否相同的,如果只重写了equals方法,那么hashcode
返回不同结果,会导致集合中元素重复。
9.1.4 toString方法
默认返回:全类名+@+哈希值的十六进制,返回值的生成算法为:
getClass().getName() + ‘@’ + Integer.toHexString(hashCode());
通常会重写,用于返回对象的属性信息
当直接输出一个对象时,toString 方法会被默认的调用
9.1.5 finalize方法
调用垃圾收集器,进行垃圾处理,当对象被判定为垃圾对象时,由JVM自动的调用此方法,用以标记垃圾对象
子类可以重写该方法,做一些释放资源的操作
9.1.6 其他方法
Object clone() 创建与该对象的类相同的新对象
Class getClass() 返回字节码对象
void notify() 唤醒等待在该对象监视器上的一个线程
void notifyAll() 唤醒等待在该对象监视器上的全部线程
wait() 让当前线程进入等待状态,释放锁,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程进入Ready(就绪)状态
9.2 包装类
八种基本类型对应的包装类:
int→Integer
char→Character
其他首字母大写
9.2.1 包装类与基本数据类型之间的转换
//以Integer和int为例,jdk1.5之前的手动装箱,int -> Integer
int n1 = 100;
Integer integer1 = new Integer(n1);
Integer integer2 = Integer.valueOf(n1);//如果在-128~127范围内,访问预先缓存的对象,在范围外则要新建对象
//手动拆箱,Integer -> int
int n2 = integer1.intValue();
//jdk1.5之后自动装箱,底层使用的是Integer.valueOf()
int n3 = 200;
Integer integer3 = n2;
//自动拆箱,底层使用的是intValue()
int n4 = integer3;
9.2.2 包装类的常用方法
System.out.println(Integer.MIN_VALUE); //返回最小值
System.out.println(Integer.MAX_VALUE);//返回最大值
System.out.println(Character.isDigit('a'));//判断是不是数字
System.out.println(Character.isLetter('a'));//判断是不是字母
System.out.println(Character.isUpperCase('a'));//判断是不是大写
System.out.println(Character.isLowerCase('a'));//判断是不是小写
System.out.println(Character.isWhitespace('a'));//判断是不是空格
System.out.println(Character.toUpperCase('a'));//转成大写
System.out.println(Character.toLowerCase('A'));//转成小写
9.3 String相关类
9.3.1 String
1.程序当中所有的双引号字符串,都是String类的对象。
2.使用Unicode字符编码,一个字符占两个字节
3.String对象底层维护private final byte[] value,所以是不可变的,改变内容都是创建了一个新的对象
4.效率低,但是复用性高,如果很少修改,被多个对象引用使用String
常用构造器:
public String(String original)
public String(char[] array)
public String(char[] array, int startIndex, int count)
public String(byte[] array)
常用方法:
//字符串内容比较
public boolean equals(Object obj)
public boolean equalsIgnoreCase(String str)//忽略大小写
//字符串获取
public int length()
public String concat(String str)//拼接
public char charAt(int index)//获取指定索引位置的单个字符(索引从0开始)
public int indexOf(String str)//查找参数字符串首次出现的索引位置,如果没有返回-1
public int lastIndexOf(String str)//查找最后出现的索引位置,如果没有返回-1
//字符串截取
public String substring(int index)//截取从参数位置一直到字符串末尾,返回新字符串。
public String substring(int begin, int end)//截取[begin,end)
//字符串转换
public String toUpperCase()
public String toLowerCase()
public char[] toCharArray()
public byte[] getBytes()
public String replace(CharSequence oldString, CharSequence newString)
public String trim()//去前后空格
//字符串分割
public String[] split(String regex)
//字符串格式化,占位符用后面变量替换 %s字符串 %c字符 %d整型 %.2f浮点型,保留两位小数
public String fomat()
String formatStr = "我的姓名是%s 年龄是%d 成绩是%.2f 性别是%c";
String resultStr = String.format(formatStr, name, age, score, gender);
9.3.1.1 字符串常量池
1.String s = "hello"先看常量池是否存在,如果存在,返回常量池的引用.如果不存在,则在池中创建并返回引用
2.String s = new String(“hello”)先在堆中创建对象,再看常量池,如果没有,创建,最后返回堆中对象引用
3.调用s.intern()时,如果池中存在与对象相等(equals)的字符串,返回池中对象引用,如果不存在,将此对象添加到池中并返回(只要记住最终返回的是常量池的对象)
9.3.1.2 创建对象问题
String a = "hello" + "123";//创建了1个对象,编译器优化
String a = "hello";
String b = "123";
String c = a + b;//共创建了三个String对象
//底层new StringBuilder().append(a).append(b).toString(),最后一步是通过字节数组的方式创建字符串,这种方式不会在池中创建对象
//总结:常量相加在池中,变量相加在堆中
9…3.2 StringBuilder
StringBuffer的简易替换,线程不安全,效率高
9.3.3 StringBuffer
1.父类AbstractStringBuilder有属性byte[] value,所以可变
2.如果对字符串存在大量修改操作使用StringBuffer和StringBuilder
//创建方式一
StringBuffer sb = new StringBuffer("hello");
//创建方式二
StringBuffer sb2 = new StringBuffer();
sb2.append("hi");
//常用方法
sb.delete(start, end);//删除索引[start,end)
sb.replace(start, end, replaceStr)//替换索引[start,end)
sb.indexOf(str)//查找字符串第一次出现索引
sb.insert(index, str)//索引处插入
sb.toString();
sb.reverse();//反转字符串
9.4 Math类
public static double abs(double num);//获取绝对值,有多种重载。
public static double pow(double num, double num2);//求幂
public static double sqrt(double num);//求开方
public static double max(double num1, double num2);//最大值
public static double min(double num1, double num2);//最小值
public static double ceil(double num);//向上取整。
public static double floor(double num);//向下取整。
public static long round(double num);//四舍五入。
public static double random();//(0.0000—1.000000]
(int)(a + Math.random()*(b-a + 1));//取a~b之间的随机数
Math.PI;//代表近似的圆周率常量(double)
9.5 Random类
Random r = new Random();
int num = r.nextInt();// 获取随机int数字
int num = r.nextInt(n);//范围[0,n)
9.6 Arrays类
public static String toString(array);//转字符串([元素1, 元素2, 元素3...])
public static void sort(array);//按照升序对数组的元素进行排序。
public static void sort(array, Comparator c);//定制排序
public static int binarySearch(array, element)//二分法进行查找,必须排好序,返回索引
public static int[] copyOf(array,array.lenth)//复制,使用的是System.arraycopy()
public static void fill(array,element);//替换
public static List<T> asList(T...a);//转成List,运行类类型java.util.Arrays#ArrayList, 是 Arrays类的静态内部类
9.7 System类
public static void exit(int status);//退出当前程序,0表示正常退出
//一般使用Arrays.copyOf(),srcPos源数组的起始位置,destPos目标数组的起始位置
public static native void arraycopy(Object src, int srcPos,Object dest,
int destPos, int length);
public static int currentTimeMillens();//返回当前时间距离1970-7-1的毫秒数
public static void gc();//垃圾回收,底层调用
9.8 BigInteger和BigDecimal类
BigInteger用来保存比较大的整型
BigDecimal用来解决浮点数运算不精确的问题
构造方法:
new BidInteger(String val);
new BigDecimal(String val);
常用方法:
add(BigDecimal bd);//加
subtract(BigDecimal bd);//减
multiply(BigDecimal bd);//乘
divide(BigDecimal bd);//除不尽时会抛异常
divide(BigDecimal bd,保留位数,舍入方式) ;//除不尽时使用
setScale(保留位数,舍入方式) ;//同上
舍入方式:
ROUND_HALF_UP 四舍五入
ROUND_HALF_DOWN 五舍六入
ROUND_HALF_EVEN 公平舍入(银行常用)比如:在5和6之间,靠近5就舍弃成5,靠近6就进位成6,如果是5.5,就找偶数,变成6
ROUND_UP 直接进位
ROUND_DOWN 直接舍弃
ROUND_CEILING(天花板) 向上取整
ROUND_FLOOR(地板) 向下取整
9.9 日期类
9.9.1 第一代Date和SimpleDateFormat
9.9.1.1 Date类
构造方法:
public Date();//精确到毫秒
public Date(long date);//表示自从1970年1月1日00:00:00以来的指定毫秒数
常用方法:
public long getTime();//把日期对象转换成对应的时间毫秒值。
9.9.1.2 SimpleDateFormat类
构造方法:
public SimpleDateFormat(String pattern);//用给定的模式和默认语言环境的日期格式符号创建
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
常用方法:
public String format(Date date);//将Date对象格式化为字符串。
public Date parse(String source);//将字符串解析为Date对象。
9.9.2 第二代Calendar类
Calendar类(抽象类,通过静态方法创建,返回子类对象)
静态方法:
public static Calendar getInstance();//使用默认时区和语言环境获得一个日历
常用方法:
public int get(int field);//返回给定日历字段的值。
public void set(int field, int value);//将给定的日历字段设置为给定值。
public void add(int field, int amount);//根据日历的规则,为给定的日历字段添加或减去指定的时间量。
public Date getTime();//返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象。
field值和含义:
Calendar.YEAR:年
Calendar.MONTH:(月,从0开始,可以+1使用)
Calendar.DAY_OF_MONTH :月中的天(几号)
Calendar.HOUR:时(12小时制)
Calendar.HOUR_OF_DAY:时(24小时制)
Calendar.MINUTE :分
Calendar.SECOND:秒
Calendar.DAY_OF_WEEK:周中的天(周几,周日为1,可以-1使用)
9.9.3 第三代LocalDateTime和DateTimeFormatter
9.9.3.1 LocalDateTime类
构造方法:
LocalDateTime ldt = LocalDateTime.now();
常用方法:
ldt.getYear();
ldt.getMonth();
ldt.getMonthValue();
ldt.getDayOfMonth();
ldt.getHour();
ldt.getMinute();
ldt.getSecond();
ldt.plusDays(666);
ldt.minusMinutes(666);
9.9.3.2 DateTimeFormatter类
构造方法:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
格式化方法:
String s = dtf.format(ldt);
9.9.3.2 Instant类
类似于Date,提供了与Date类互相转换的方法
Instant instant = Instant.now();
//Instant->Date
Date date = Date.from(instant)
//Date->Instant
Instant instant = date.toInstant();
10 枚举和注解
10.1 枚举
public enum Week {
MONDAY("星期一"),TUESDAY("星期二"),WEDNESDAY("星期三"),
THURSDAY("星期四"),FRIDAY("星期五"),SATURDAY("星期六"),SUNDAY("星期天");
//等价于public static Week MONDAY = new Week("星期一");
//如果使用无参构造器,()也可以省略
private String name;
Week(String name) {
this.name = name;
}
week() {
}
@Override
public String toString() {
return name;
}
}
使用枚举时,会隐式继承Enum类,其中的常用方法:
name:返回当前对象(常量)名,子类不能重写,建议使用toStirng()
ordinal:返回当前对象位置号
values:返回所有常量的数组
valuesOf:将字符串转换成枚举对象,要求字符串必须为已有的常量名
compareTo:返回当前枚举常量编号减去传入枚举常量编号
10.2 注解
注解(Annotation),也叫元数据。一种代码级别的说明。
它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。
它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
本质是继承Annotation接口的一个接口:public interface MyAnno extends java.lang.annotation.Annotation {}
10.2.1 JDK预定义注解
@Override:检测被该注解标注的方法是否是继承自父类(接口)的
@Deprecated:表示某个程序元素已过时
@SuppressWarnings:压制警告,一般传递参数all @SuppressWarnings(“all”)
10.2.2 自定义注解
public @interface 注解名称{
//属性列表;
//属性:接口中的抽象方法,返回值类型有基本数据类型,String,枚举,注解,以上类型的数组
//注意:
//1.定义了属性,在使用时需要给属性赋值
//2.如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性 // 的赋值。
//例:String name() default "张三";
}
10.2.3 元注解
元注解指的是用于描述注解的注解
@Target:描述注解能够作用的位置
如:@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
@Retention:描述注解被保留的阶段(SOURCE在源文件中有效,CLASS在字节码文件中有效,RUNTIME在运行时有效)
如:@Retention(RetentionPolicy.RUNTIME)
@Documented:描述注解是否被抽取到api文档中
@Inherited:描述注解是否被子类继承
10.2.4 注解的使用
1.如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
2.数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略
11 异常
11.1 异常体系结构
异常的根类Throwable
两个子类:Error和Exception
Exception下子类:RuntimeException,其他子类均被称为CheckedException(没有这个类)
常见的运行时异常:
NullPointException
ArithmeticException
ArrayIndexOutOfBoundsException
ClassCastExcepiton
NumberFormatException(Integer.paseInt(这里的字符串不能转成数字时抛出))
常见的编译异常:
SQLException
IOException
FileNotFoundException
EOFException(操作文件时到末尾)
ClassNotFoundException
11.2 异常处理
11.2.1 try-catch-finally
try{
//可能会出现异常的代码
}catch(异常类型 e){
//出现了异常,进入到catch
//记录日志/打印异常信息/继续抛出异常
//可以有多个catch语句,子类异常在前,父类异常在后
}finally{
//一定会执行,释放资源
}
1.可以try-finally配合使用,相当于没有捕获异常
2.try或catch中的return要返回的值在finally执行之前会拷贝一份。 如果在finally中返回,会覆盖掉之前的返回值.(记住finally是一定会执行的)
11.2.2 throws
在方法声明中声明抛出异常的类型,由该方法的调用者负责处理,是java默认的异常处理方式
11.2.2.1 throw和throws的区别
throw:手动抛出异常对象的关键字,用于方法体中,后面跟异常对象.
throw new NullPointerException("要访问的arr数组不存在");
throws:表示可能抛出异常,是异常处理的一种方式,用于方法声明处,后面跟异常类型
public void method() throws FileNotFoundException{};
11.3 自定义异常
class MyException extends RuntimeException {
public MyException(String message) {
super(message);//通过throw手动抛出
}
}
如果继承Exeception,属于编译异常
如果继承RuntimeException,属于运行异常(一般使用)
12 集合
12.1 集合的框架体系
单列集合的体系结构
Collection根接口:
子接口List ->
实现类ArrarList(数组)
实现类LinkedList(链表)
实现类Vector
子接口Set ->
实现类HashSet(哈希表+红黑树) ->
子类LinkedHashSet(哈希表+链表,可以保存存取顺序)
实现类TreeSet(二叉树,用于排序)
双列集合的体系结构
Map根接口:
实现类HashMap ->
子类LinkedHashMap
实现类TreeMap
实现类Hashtable ->
子类Properties
12.2 Collection接口常用方法
add();//添加
remove();//删除首次出现的指定元素
clear();//清空
contain(E e);//是否包含
isEmpty();//是否为空
size();//获取长度
Object[] toArray();//转成数组
12.3 Collection接口两种遍历方法
1.迭代器(快捷键itit)
Iterator<String> it = coll.iterator();//通过集合获取迭代器对象
while (it.hasNext()) {//通过循环遍历迭代器,判断迭代器对象中是否还有元素
String str = it.next();//指针后移一位并返回这个位置的元素
System.out.printfln(str);
}
2.增强for
for(数据类型 变量名 : 容器对象) {
//循环体语句
}
12.4 List接口
特点:有索引,可以储存重复元素,可以保证存取顺序
特有方法:
public void add(int index, E element);//添加元素
public E remove(int index);//移除元素
public E set(int index, E element);//替换元素
public E get(int index);//获取元素
public int indexOf(Object obj);//首次出现位置
public List<E> subList(int fromIndex, int toIndex);//包括前不包括后
12.4.1 ArrayList
底层数据结构:
transient Object[] elementData;
扩容机制:
当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次往集合中添加元素时,扩容为10.如果使用的是指定大小的构造器,则初始化容量为指定大小.之后按照1.5倍扩容.
12.4.2 Vector
底层数据结构:
protect Object[] elementData;
扩容机制:
当创建Vector对象时,如果使用的是无参构造器,则初始elementData容量为10.如果使用的是指定大小的构造器,则初始化容量为指定大小.之后按照2倍扩容
特点:
通过synchronized保证线程安全但是效率不高
12.4.3 LinkedList
底层数据结构:双向链表,每个节点都是一个Node对象,first和last分别指向首节点和尾节点
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;
}
}
/**
* Pointer to first node.
*/
transient Node<E> first;
/**
* Pointer to last node.
*/
transient Node<E> last;
特有方法:
void addFirst(E e);//将指定元素插入此列表的开头
void addLast(E e);//将指定元素添加到此列表的结尾
E getFirst();//返回此列表的第一个元素
E getLast();//返回此列表的最后一个元素
E removeFirst();//移除并返回此列表的第一个元素
E removeLast();//移除并返回此列表的最后一个元素
12.4.3.2 ArrayList和LinkedList进行比较
ArrayList是数组结构,支持根据下标随机访问,进行查询操作时,同样都是O(N),但因为数组寻址快,且数组是连续的存储空间,会有一部分或全部数据进入CPU缓存,数组会比链表快.但是进行增删操作时,需要把插入/删除点之后的数据全部复制并后/前移一位,消耗性能
LinkedList是链表结构,增删数据只需要改变next和prev的指向,效率快,查询时,会将查询的位置与总长度的一半进行比较,然后决定从首/尾节点开始遍历,虽然进行了优化,但在数据量大的情况下还是消耗时间.
- 时间复杂度
操作 | 数组 | 链表 |
---|---|---|
随机访问 | O(1) | 不支持 |
查找 | O(N) | O(N) |
头部插入/删除 | O(N) | O(1) |
尾部插入/删除 | O(1) | O(1) |
12.4.3.1 ArratList和LinkedList哪个占用空间大
一般情况下,LinkedList的占用空间更大,假设都存储Object类型的对象,因为一个LinkedList的节点对象需要保存前后两个节点的引用和对应Object的引用.而ArrayList最坏的情况是数组中的一个元素相当于保存了1.5个Object的引用.不过,elemenData数组是用transient关键字修饰的,序列化的时候,多余的空间不会被序列化,可以节省空间
12.5 Set接口
无索引,不可以储存重复元素,存取无序
12.5.1 HashSet
底层数据结构:HashMap,其中的value使用静态Object对象
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing {@code HashMap} instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
12.5.1.1 LinkedHashSet
底层数据结构:LinkedHashMap
12.5.3 TreeSet
底层数据结构:TreeMap
12.6 Map接口
特点:
1.K-V结构,K不可以重复
2.可以存储null键和null值
12.6.1 Map接口常用方法
public V put(K key, V value);
public V remove(Object key);//返回被删除元素
public V get(Object key);//键取值
public boolean containsKey(Object key);//查找键是否存在
public Collection<V> valus();//获取所有value的集合
public Set<K> keySet();//获取Map集合中所有的键,存储到Set集合中。
public Set<Map.Entry<K,V>> entrySet()//获取Map集合中所有的键值对对象的Set集合
12.6.2 Map接口两种遍历方式
1.键找值方式:即通过元素中的键,获取键所对应的值
Set<String> keys = map.keySet();
for (String key : keys) {
String value = map.get(key);
System.out.println(key+"="+value);
}
2.Entry键值对对象:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。
Set<Map.Entry<String,String>> entrySet = map.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"="+value);
}
12.6.3 HashMap
底层数据结构:哈希表(数组+链表/红黑树),每个元素都是一个Node对象(hashmap的内部类),且实现了Map接口的内部接口Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
添加机制:
先得到hash,算法是高16位异或低16位,好处可以在数组table的length比较小的时候(指的是后面和lengh-1按位与),也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
使用异或的原因是&结果偏向于0,|结果偏向于1,^运算的结果0和1概率相同
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//h:1111 1111 1111 1111 1111 0000 1110 1010
//h>>>16:0000 0000 0000 0000 1111 1111 1111 1111
//h^(h>>>16):1111 1111 1111 1111 0000 1111 0001 0101
接下来计算出存放的索引值,算法如下.当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//tab.length-1(16):0000 0000 0000 0000 0000 0000 0000 1111
//h:1111 1111 1111 1111 0000 1111 0001 0101
//(tab.length-1) & h:0101=5
如果数组索引处是否有已经存放的元素,如果没有,直接加入,如果有,则遍历链表调用equals比较,如果相同,不添加(hashSet),hashMap则会覆盖掉value的值,如果不相同,则添加到最后.即保证元素唯一性的方式依赖于hashCode 与 equals 方法.
扩容/树化机制:
如果没有指定大小,第一次添加时,哈希桶数组容量16,默认加载因子0.75,超过按2倍扩容.加载因子0.75是为了在查询时间和存储空间上寻找折中,兼顾哈希冲突和存储空间利用率
如果链表元素到达8,且数组长度到达64,转成红黑树,否则还是按照数组的扩容机制扩容
12.6.3.1 为什么HahMap长度一定是2的n次幂?
为了方便执行优化算法(n-1)&hash,长度16或者其他2的幂,length-1的值是所有二进制位全为1,这种情况下,存放的位置等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
(&效率高,hash分布均匀)
12.6.3.2 HashMap的线程安全问题
扩容时其他线程也在扩容,有可能形成环形链表,下一次读操作时有可能出现死循环
原理:线程A扩容,将一条链表A->B->C上的节点按头插法形成C->B->A,此时线程B醒了,保留阻塞之前形成的A->B,但此时已经是线程A的链表了,B指向A,就形成环状
解决办法:
jdk1.7是头插法(因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率),jdk1.8是尾插法
//判断环形链表
public boolean isLoop(Node head) {
Node slow = head;
Node fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if(slow == fast) {
return true;
}
}
return false;
}
即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,无法保证线程安全
12.6.3.3 解决哈希冲突的办法
1.拉链法
-
由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况
-
开放定址法为减少冲突,要求装填因子α较小,而拉链法中可取α≥1,节省空间
-
在用拉链法构造的散列表中,删除结点的操作易于实现,只要简单地删去链表上相应的结点即可
2.开放地址法,在插入一个元素的时候,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。
3.二次哈希法,发生冲突时通过哈希函数再次散列,直到不发生冲突为止
12.6.3.4 LinkedHashMap
底层数据结构:数组+双向链表,链表可以保证存取顺序
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
nmvvEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
transient LinkedHashMap.Entry<K,V> head;//头节点
transient LinkedHashMap.Entry<K,V> tail;//尾节点
12.6.4 HashTable
使用数组+链表存放entry对象,第一次添加初始化容量为11,加载因子0.75,按2倍+1扩容,键与值都不能为空,通过synchronized保证线程安全.
建议多线程环境下使用ConcurrentHashMap
12.6.4.1 Properties
1.HashTable的子类,特点与HashTable类似
2.可从xx.properties文件加载数据到Properties对象进行读/写操作
构造方法:
public Properties();//创建一个空的属性列表。
储存方法:
public Object setProperty(String key, String value);//保存一对属性,调用 Hashtable的put
public String getProperty(String key);//通过key返回value值,get(key)
public Set<String> stringPropertyNames();//返回所有键的名称的集合,keySet
与流有关的方法:
public void load(InputStream inStream);//不能读取含有中文的键值对
public void load(Reader reader);//能读取含有中文的键值对
public void store(OutputStream out, String comments);//不能写入中文
public void store(Writer writer, String comments);//可以写入中文
//String comments:注释,用来解释说明保存的文件是做什么用的
//不能使用中文,会产生乱码,默认是Unicode编码,一般使用""或null
12.6.5 TreeMap
是红黑树算法的实现
自然排序:
元素必须实现Comparable接口,并根据元素的compareTo方法比较大小,不能传入null键
定制排序:
使用匿名内部类传入Comparator接口的实现类并重写compare方法
TreeMap<String, String> treeMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
如果要按照升序排序:前减后
如果要按照降序排序:后减前
12.6.5.1 红黑树
特点:
平衡二叉B树
每一个节点可以是红或者黑
红黑树不是高度平衡的,它的平衡是黑色平衡
红黑规则:
1.根节点必须是黑色
2.如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
3.如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况)
4.对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
添加节点默认颜色:
默认为红色,效率高
添加节点如何保持红黑规则:
根节点位置
直接变为黑色
非根节点位置
父节点为黑色
不需要任何操作,默认红色即可
父节点为红色
叔叔节点为红色
1.将"父节点"设为黑色,将"叔叔节点"设为黑色
2.将"祖父节点"设为红色
3.如果"祖父节点"为根节点,则将根节点再次变成黑色
叔叔节点为黑色
1.将"父节点"设为黑色
2.将"祖父节点"设为红色
3.以"祖父节点"为支点进行旋转
12.7 Collections工具类
public static <T> boolean addAll(Collection<T> c, T... elements);//向集合中添加另一个集合的所有元素
public static void shuffle(List<?> list);//打乱顺序
public static void reverse(List<?> list);//反转集合元素
public static <T> void sort(List<T> list);//自然排序
public static <T> void sort(List<T> list,Comparator<? super T>);//定制排序
public static <T> List<T> synchronizedList(List<T> list);//获取线程安全的list
public static <T> Set<T> synchronizedSet(Set<T> s);//获取线程安全的set
13 泛型
可以在类声明时通过一个标识表示类中某个属性的类型,或者某个方法的返回值/参数类型
本质是参数化类型,也就是说所操作的数据类型被指定为一个参数传递
好处:在编译的时候能够检查类型安全,减少了类型转换的次数
13.1泛型类
class 类名<T>{
T t;
}//泛型类,创建对象确定类型,没有指定,默认Object
13.2 泛型方法
public <T> void method(T t){}//声明泛型方法,被调用时确定类型
public void method2(E e){}//不是泛型方法,而是使用了泛型,需要在泛型类中使用
13.3 泛型接口
public interface 接口名<T> {
//继承或者实现接口时确定类型
}
13.4 常用泛型标识符
T:表示具体的一个java类型
K V:Map集合中的Key Value
E:集合中的Element
N:数值类型
13.5 泛型通配符
<?>用于方法的参数,表示可以接收任意的类型public static void printList(List<?> c) {
for (Object object : c) {
System.out.println(object);
}
}
泛型的上限:类型名称 <? extends 类 > 对象名称(只能接收该类型及其子类)
泛型的下限:类型名称 <? super 类 > 对象名称(只能接收该类型及其父类)
13.6 泛型的类型擦除
所有的泛型类型在编译后都会被清除掉,字节码文件中没有泛型的信息,编译器承担了全部的类型检查工作
14 单元测试
@Test
public void testMethod() {
System.out.println("testMethod...");//要求返回值为void,参数列表为空
}
15 多线程
15.1 线程相关概念
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,是操作系统资源分配的最小单位(现在计算资源已经分配给线程了)
线程:是进程中的一个执行单元,一个进程中可以有多个个线程,各个线程之间共享线程的内存空间,是程序执行的最小单位
并发:一个处理器同时处理多个任务,在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果
并行:多个处理器或者是多核的处理器同时处理多个不同的任务。同一时刻,有多条指令在多个处理器上同时执行。
内核:是应用程序和硬件沟通的桥梁。应用程序不断通过内核调用请求硬件资源,这个时候内核中也需要有线程去承接这些任务
内核级线程:
内核级线程由操作系统负责调度,只能运行在内核态(内存的内核空间)
用户级线程:
用户级线程指由应用程序自己调度线程,只能运行在用户态(内存的用户空间)
Java的线程是内核级线程。操作系统帮助Java把用户级线程和内核级线程映射起来。用m个内核级线程来执行n个JVM线程的程序。实际上Linux和Windows上,m:n是1:1.对JVM来说,主线程和各个子线程是平行的,公平竞争CPU资源,可以利用多核优势。
线程调度:
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
15.2 创建线程的方式
15.2.1 继承Thread类
public class Test {
public static void main(String[] args) {
new MyThread().start();//底层调用start0()方法开启新线程,变成可运行状态
}
}
class MyThread extends Thread {
@Override
public void run() {
//重写run方法
}
}
//好处是可以直接使用Thread类的方法
15.2.2 实现Runnable接口
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
//重写run方法
}
});
//这种方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制
15.2.2 实现Callable接口
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
myCallable myCallable = new myCallable();
FutureTask<Object> futureTask = new FutureTask<>(myCallable);//获取线程执行完毕之后的结果,传递给Thread对象
Thread t = new Thread(futureTask);
t.start();
Object o = futureTask.get();//必须等线程执行完才能获取结果
}
}
class myCallable implements Callable<Object> {
@Override
public Object call() throws Exception {
return null;
}
}
//可以返回值,也可以抛出异常,通过FutureTask对象可以获取任务执行结果
15.3 Thread类
构造方法:
public Thread();
public Thread(String name);
public Thread(Runnable target);
public Thread(Runnable target,String name);
常用方法:
public String getName();
public void start();
public void run();//此线程要执行的任务在此处定义代码。
public static void sleep(long millis);//使当前正在执行的线程以指定的毫秒数暂停
public static Thread currentThread();//返回对当前正在执行的线程对象的引用
public static native void yield();//线程的礼让,让其他线程执行,不一定成功
public void join();//插队,先执行完拆入线程所有的任务
public final int getPriority();//返回此线程优先级
public final void setPriority(int newPriority);//更改线程优先级,默认5(1~10)
//设置守护线程,当所有用户线程结束,守护线程自动结束,垃圾回收机制就是一个守护线程
public void setDaemon(boolean on);
15.4 线程状态
NEW(新建):线程刚被创建,但是并未启动。还没调用start方法
Runnable(可运行):在Running,Ready中切换
Blocked(锁阻塞):当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则进入该状态
Waiting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入:Object.wait() Thread.join() 退出:Object.notify() Object.notifyAll()
Timed Waiting(计时等待):同waiting状态,Thread.sleep 、 Object.wait有超时参数,调用他们将进入Timed Waiting状态。
Terminated(被终止):因为run方法正常退出而终止,或者因为没有捕获的异常终止了run方法而终止。
15.5 线程同步
有多个线程的多条代码操作一个共享数据时,只允许一个线程执行
15.5.1 Synchronized
实现的锁:
悲观锁,非公平锁,可重入锁,独占锁/排他锁
同步代码块:
synchronized(同步锁){
//需要同步操作的代码(锁对象任意,多个线程的锁对象为同一个即可)
}
同步方法:
public synchronized void method(){
//可能会产生线程安全问题的代码(默认锁对象this,只能锁定调用该方法的对象)
}
public static synchronized void method(){
//默认锁对象类名.class
}
释放Synchronized锁的情况:
1.当前线程的同步方法/代码块执行结束
2.当前线程在同步方法/代码块中遇到break,return
3.当前线程在同步方法/代码块中出现了未处理的Error或Exception
4.当前线程在同步方法/代码块中执行了wait()方法
不会释放锁:
1.当前线程在同步方法/代码块中调用Thread.sleep(),Thread.yield()方法
2.当前线程在同步方法/代码块中其他线程调用了该线程的suspend()方法将该线程挂起
15.5.1.1 Synchronized的实现原理
实现原理:
1.每个对象都存在一把锁/Monitor,这把锁的信息存放在对象头的markword(32bit)中,也可以说每一个对象都和一个Monitor关联
2.在HotSpot虚拟机中monitor对象由ObjectMonitor实现,结构如下
ObjectMonitor() {
_count = 0; //锁计数器
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。当一个线程需要获取锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner,可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,可以调用notify方法使wait set中的线程重新进入Entry竞争锁
15.5.1.2 Synchronized锁升级的过程
无锁——>偏向锁——>轻量级锁——>重量级锁
偏向锁:
偏向锁是最轻量级的锁,它应用在一个锁对象的生命周期内只会被一个线程获取的场景下,其加锁过程如下:
首先获取锁的线程会先检查锁对象Mark World中的偏向模式标志位是否为可偏向,0为不可偏向,会升级到轻量级锁;1为可偏向,分为以下两种情况:
锁对象在第一次被线程获取,那么其可偏向状态为1,此时Mark World中没有记录某个线程的ID,当前线程会使用CAS尝试将自己的线程ID记录在Mark World中,成功则直接进入同步代码块中执行。
锁对象不是第一次被线程获取时,线程会检查Mark World中的线程ID是否是自己的,如果是则说明没有第二个线程获取过该锁,直接进入同步代码块执行;如果不是那么说明可能是一个多线程环境,需要撤销偏向模式,也即将标志位改为0,并升级到轻量级锁
轻量级锁:
轻量级锁应用在同步代码块执行期间,只会有一个线程去获取锁的场景下,加锁过程如下:
线程会在栈帧中保存Mark World的副本,称为锁记录(Lock Record)。然后使用CAS尝试将Mark World更新为指向锁记录的指针:
1.1 若CAS成功则说明没有其他线程竞争锁,则直接进入同步代码块执行。
1.2 若CAS失败则可以分成两种情况:
(1)当前线程重入了该锁,也就是说此时的Mark World是指向当前线程栈帧Lock Record的指针,而栈帧中的Lock Record则是锁对象原本的Mark World的内容,二者当然不同,CAS会失败。但是当前线程是持有该轻量级锁的,因此JVM需要在CAS失败后检查Mark World是否是指向当前线程的指针,如果是,则直接进入同步代码块执行;如果不是,那么就是下面的情况。
(2)这是一个多线程环境,此时的Mark World是指向别的线程Lock Record的指针,需要升级到重量级锁,需要将Mark World更新为指向重量级锁的指针,此后获取该锁的线程会阻塞。
轻量级锁会有一个锁释放的过程,当拥有轻量级锁的线程执行完同步代码之后,会使用CAS将栈帧中的锁记录替换回去,如果成功则说明同步代码执行过程中,没有其他线程获取过该锁,同步完成。
如果锁释放失败,那么说明有其他线程获取过该锁,此时Mark World是指向重量级锁的指针,已经升级到重量级锁,需要唤醒其他被阻塞的线程。
重量级锁:
重量级锁是阻塞同步,不占有锁的线程需要陷入阻塞。由于主流JVM使用的是操作系统的原生内核线程,对内核线程的阻塞、唤醒都需要操作系统内核的支持。
陷入内核态需要先保存当前线程的运行环境,然后再切换到内核的运行环境,这是一个十分耗费时间的过程,有时候甚至超过了同步代码本身用时,是一个十分重量的操作,因此得名重量级锁
轻量级锁和偏向锁的区别:
偏向锁是在一个极端的环境下才会应用的,也即一个锁对象从诞生到死亡都只会有一个线程获取它。如果有第二个线程获取那么则会结束偏向,也就是说偏向锁只能被一个线程获取,且只在第一次获取时进行CAS操作
轻量级锁则是应用在同时只会有一个线程执行同步代码的情况下,在轻量级锁被释放后是可以被其他线程获取的,如果轻量级锁的拥有者在执行同步代码的过程之中又有其他线程申请该锁,轻量级锁才会失效。轻量级锁每次申请都需要进行CAS,释放时也需要CAS
15.5.2 ReentrantLock
实现原理:基于AQS实现,通过循环调用CAS操作来实现加锁
基本使用:
public class ReentrantLockTest {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> test(),"线程A").start();
new Thread(() -> test(),"线程B").start();
}
public static void test() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获取了锁");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
lock.unlock();
}
}
}
功能:
非公平锁实现:new一个ReentrantLock的时候参数为true,谁等的时间最长,谁就先获取锁
可中断: t.interrupt();可以实现中断
限时等待:lock.tryLock(超时时间,时间单位);以自旋方式获得锁
15.5.3 线程间通信
wait/notify/notifyAll必须先获得该线程的对象锁,即需要在同步代码块中使用
public class Test {
static Object object = new Object();
static int count = 1;
public static void main(String[] args) {
//线程 A 消费者
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
count--;
if (count == 0) {
object.wait();
}
} catch (Exception e) {
}
}
}
}).start();
//线程 B 生产者
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
count++;
object.notify();
} catch (Exception e) {
}
}
}
}).start();
}
}
15.5.4 死锁
两个线程即将请求的资源被对方占有
ublic class DeadLock {
public static void main(String[] args){
Object A = new Object();
Object B = new Object();
new Thread(() -> {
synchronized (A) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("success");
}
}
}).start();
new Thread(() -> {
synchronized (B) {
synchronized (A) {
System.out.println("success");
}
}
}).start();
}
}
15.5.5 ThreadLocal
ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量
static final ThreadLocal<T> threadLocal = new ThreadLocal<T>();
threadLocal.set()
threadLocal.get()
实现原理:
每个线程都有ThreadLocals变量是一个ThreadLocalMap类型的变量,K是ThreadLocal本身,V是设置的值
内存泄漏问题:
ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的V不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
15.6 线程池
降低资源消耗,提高响应速度,提高线程的可管理性
15.6.1 Executors创建线程池
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();//可缓存线程池,最多int最大值个线程
//ExecutorService executorService = Executors.newFixedThreadPool(10);定长线程池,可控制线程最大并发数
//ExecutorService executorService = Executors.newScheduledThreadPool(10);//定长线程池,支持定时及周期性任务执行
//ExecutorService executorService = Executors.newSingleThreadExecutor();创建单线程化的线程池,只会用唯一一个工作线程执行任务
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "正在运行");
});//传递Runnable接口自动创建线程对象
executorService.shutdown();//关闭线程池
}
}
15.6.2 ThreadPoolExecutor
ExecutorService的默认实现
15.6.2.1 阻塞队列
Iterable -> Collection -> Queue -> BlockingQueue:
实现类ArrayBlockingQueue: 底层是数组,有界
实现类LinkedBlockingQueue: 底层是链表,无界,最大为int的最大值
BlockingQueue的核心方法:
void put(E e);//将参数放入队列,如果放不进去会阻塞
E take();//取出第一个数据,取不到会阻塞
15.6.2.2 线程池参数
public static void main(String[] args) {
//参数一:核心线程数量,正式工
//参数二:最大线程数,大于等于核心线程数量
//参数三:空闲线程最大存活时间
//参数四:时间单位
//参数五:任务队列
//参数六:创建线程工厂
//参数七:任务的拒绝策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
pool.submit(new MyRunnable());
pool.shutdown();
}
执行流程:corePoolSize -> queue(进入队列等待) -> maxPoolSzie
一个线程池 core 7, max 20, queue 50, 100并发进来怎么分配的:
7个会立即执行。50个进入队列,在开13个进行执行。剩下30个就是用拒绝策略
15.6.2.3 线程池状态
RUNNING //当创建线程池后初始状态
SHUTDOWN //不能够接受新的任务,它会等待所有任务执行完毕
STOP //不能接受新的任务,不再处理等待队列中的任务,并且会去尝试终止正在执行的任务
TIDYING //该状态下所有任务都已终止或处理完成,将会执行terminated()方法
TERMINATED //当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
15.6.2.4 任务拒绝策略
当提交的任务>最大线程数+队列容量
调用RejectedExecutionHandler:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,是默认的策略
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常,不推荐
ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行
15.7 volatile关键字
15.7.1 JMM
1.堆内存是唯一的(主存),但是每个线程都有自己的线程栈(工作内存)
2.每一个线程在使用堆里面变量的时候,都会先拷贝一份到自己的栈中
3.修改的是工作内存的变量,JMM控制更新到主存的过程
15.7.2 volatile的作用
1.使用volatile修饰共享变量后,当线程操作变量副本并写回主内存后,会告知其他线程该变量副本已经失效,需要重新从主内存中读取,保证了不同线程对共享变量操作的可见性
2.编译生成字节码文件时插入内存屏障禁止指令重排序,保证了有序性
class Money {
//如果不加volatile,由于线程1没有及时获取更新后的值,程序会直接卡住
public static volatile int money = 100000;
}
class MyThread1 extends Thread {
@Override
public void run() {
while(Money.money == 100000){//原子性操作,可用volatile
}
System.out.println("已经不是十万了");
}
}
class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;//原子性操作,可用volatile
}
}
15.7.3 原子性,可见性,有序性
1.使用synchronized既可以保证原子性,也可以保证内存可见性,线程获得锁后会把变量副本清空并拷贝最新的值,执行完修改操作后将变量副本的值更新给共享数据
2.synchronized可以保证有序性,无法禁止指令重排序,但在单线程环境中发生指令重排序没有问题
3.volatile不能保证原子性(只有简单的读取、赋值是原子操作,不可以被打断),即不能保证线程安全,可以通过加锁解决,如
//两个线程同时自增100次,最后结果为2~200
//即使加了volatile,每次操作时都保证能拿到最新的数据,但拿到最新的数据后还是有可能会被其他线程打断并修改i的值
//假设从A开始交替执行,结果为2的过程如下,终极理解:因为修改变量最终会刷新主存,这个过程分别是0-1和1-2
//A 0-1(未刷新) 1-99(每次都刷新) 1-2(未刷新) 2(执行刷新)
//B 0-1(未刷新) 1(执行刷新) 1-100(每次都刷新)
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 2; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0;
//private Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//synchronized (lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(count);
//}
}
}
}
15.7.4 volatile在单例模式中的应用
new对象时,有以下几个步骤:
1.分配内存空间
2.调用构造函数
3.返回地址给引用
编译器可能对2,3步进行指令重排序,假如A线程执行到new的时候,B线程调用getInstance()方法,会返回一个未被初始化的对象
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {//这步是为了节约资源
synchronized (Singleton.class) {//加锁,保证线程安全
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
15.8 原子类
AtomicInteger
AtomicBoolean
AtomicLong
AtomicReference
构造方法:
public AtomicInteger();//初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue);//初始化指定值
常用方法:
public int get();//获取值
public int getAndIncrement();//返回自增前的值
public int incrementAndGet();//返回自增后的值
public int addAndGet(int data);//相加并返回结果
public int getAndSet(int value);//设置新值,返回旧值
15.8.1 原子类原理:CAS算法
CAS(Compare And Swap)算法:
CPU指令提供的原子操作,Java中由Unsafe类提供,乐观锁的体现
修改主存的变量时,判断
1.获取的旧址==内存值,修改成功
2…获取的旧址!=内存值,说明有其他线程进行了操作,获取新值继续尝试更新
问题:ABA
Java解决方案:比较时带版本号
AtomicMarkableReference(版本号为布尔)
AtomicStampedReference(版本号为整数)
15.9 并发安全容器
15.9.1 CopyOnWriteArrayList/CopyOnWriteArraySet(写入时复制)
1.读操作不加锁,内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全
2.写操作加可重入锁,保证线程安全
3.读写不互斥原理:当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了
优点:适合读多写少
缺点:只能保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据
15.9.2 ConcurrentHashMap
15.9.2.1 JDK1.7原理
Segment分段锁
创建:
1.默认创建一个长度为16,加载因子为0.75的Segment[]数组,无法扩容
2.创建一个长度为2的HashEntry[]数组,赋值给0索引,加载因子0.75,按两倍扩容
添加:
1.根据key计算出在Segment数组中的索引,获取可重入锁
2.如果为null,创建HashEntry数组,二次哈希,计算索引存入
3.如果不为null,二次哈希计算索引,如果需要扩容,则扩容,如果不需要扩容,调用equals方法比较后存入,形成数组+链表的结构
4.释放获取的Segment锁
多个线程如何保证安全:Segment继承了ReentrantLock,多个线程插入时,会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒
Size方法一致性问题:
1.不加锁统计Segment数组的所有元素和修改次数
2.比较当前所有Segment的修改次数是否大于上一次,如果相同,就结束,不相同,持续尝试3次
3.如果3次都不相同,对每个Segment加锁,计算大小
15.9.2.2 JDK1.8原理
1.实现原理和jdk1.8的hashmap基本相同,并发控制使用synchronized和CAS
2.每个Nod节点中的value和next都用volatile修饰,保证内存可见性
添加:
1.如果索引位置null,利用CAS算法添加到数组中
2.如果不为null,使用Synchroized关键字来同步代码块,使用头结点作为同步锁对象
16 IO流
16.1 File类
构造方法:
public File(String pathname);
public File(String parent, String child);
public File(File parent, String child);
常用方法:
//获取方法
public String getPath();//转换为路径名字符串,File类的toString方法就是调用了此方法
public String getName();//返回由此File表示的文件或目录的名称。
public File getParentFile();//返回父路径文件
public long length();//返回由此File表示的文件的长度。
//判断方法
public boolean exists();//此File表示的文件或目录是否实际存在。
public boolean isDirectory();//此File表示的是否为目录。
public boolean isFile();//此File表示的是否为文件。
//创建及删除方法
public boolean createNewFile();//当且仅当具有该名称的文件尚不存在时,创建
public boolean delete();//删除由此File表示的文件或目录(必须为空)。
public boolean mkdir();//创建由此File表示的目录。
public boolean mkdirs();//创建由此File表示的多级目录。
//遍历方法
public String[] list();//返回一个String数组,表示该File目录中的所有子文件或目录。
public File[] listFiles();//返回一个File数组,表示该File目录中的所有的子文件或目录。
过滤器:
File[] listFiles(FileFilter filter);
//抽象方法
boolean accept(File pathname);//测试pathname是否应该包含在当前File目录中,符合则返回true。
//File pathname是使用ListFiles方法遍历目录,得到的每一个文件对象
实际使用:
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java") || pathname.isDirectory();
}
});
过程:
1.listFiles方法对File对象的构造方法中传递的目录进行遍历,获取每一个文件/文件夹,封装成file对象
2.调用参数中传递的过滤器对象的方法accept,把每一个file对象传递给accept的参数pathname
3.accept方法返回值是true的话将传递过去的对象保存在数组中
16.2 InputStream(字节输入流)
16.2.1 FileInputStream
构造方法:
FileInputStream(File file);
FileInputStream(String name);
//如果没有该文件,会抛出 FileNotFoundException
读取方法:
int read();//每次读取一个字节,提升为int类型,读取到文件末尾,返回 -1
int read(byte[] b);//每次读取b的长度的字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回 -1
//循环读取
while ((b = fis.read())!=‐1) {
System.out.println((char)b);
}
//数组方式循环读取
int len;
byte[] b = new byte[2];
while (( len= fis.read(b))!=‐1) {
//每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b,0,len));
}
16.2.2 BufferedInputStream
构造方法:
public BufferedInputStream(InputStream in);//创建一个新的缓冲输入流。
16.2.3 ObjectInputStream
构造方法:
public ObjectInputStream(InputStream in);
反序列化方法:
public final Object readObject ();//读取一个对象。
InvalidClassException异常出现原因:UID不一致,加载类时对类信息作了修改
解决方法:加入序列版本号
private static final long serialVersionUID = 1L;
16.3 OutputStream(字节输出流)
16.3.1 FileOutputStream
构造方法:
public FileOutputStream(File file);
public FileOutputStream(File file, boolean append);//追加续写开关
public FileOutputStream(String name);
public FileOutputStream(String name, boolean append);
//该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据
写出方法:
write(int b);//每次写出一个字节数据,int类型截取后8位
write(byte[] b);//写出字节数组中的数据
write(byte[] b, int off, int len) ;//写出指定长度字节数组,每次写出从off索引开始,len个字节
//如果写的第一个字节是正数(0-127),那么显示的时候会查询ASCII表
//果写的第一个字节是负数,那么两个字节节组成一个中文显示,查询系统默认码表(GBK)
//换行:fos.write("\r\n".getBytes());
16.3.2 BufferedOutputStream
构造方法:
public BufferedOutputStream(OutputStream out);//需要通过flush或者close方法刷新缓冲区
16.3.3 ObjecOutputStream
构造方法:
public ObjectOutputStream(OutputStream out);
序列化方法:
public final void writeObject (Object obj);//将指定的对象写出。
//1.该类必须实现Serializable接口
//2.如果有一个属性不需要可序列化的,该属性使用transient瞬态关键字修饰。
16.3.4 PrintStream
关于System.out.println()的说明:
System是java.lang里面的一个类,out就是System里面的一个静态数据成员,而且这个成员是java.io.PrintStream类的引用.
println()就是java.io.PrintStream类里的一个方法,它的作用是向控制台输出信息
因为System.out是java.io.PrintStream类的实例的引用,所以可以通过 System.out.println();来调用此方法。
构造方法:
PrintStream(File file);//输出的目的地是一个文件
PrintStream(OutputStream out);//输出的目的地是一个字节输出流
PrintStream(String fileName);//输出的目的地是一个文件路径
改变打印流向:
public static void setOut(PrintStream out);//System类中的方法,重新分配System.out打印输出流
//使用方式
System.setOut(new PrintStream("e:\\f.txt"));
System.out.println("改变了打印流向");
16.4 Reader(字符输入流)
16.4.1 FileReader
构造方法:
FileReader(File file);
FileReader(String fileName);
读取方法:
int read();//每次读取一个字符,提升为int类型,读取到文件末尾,返回 -1
int read(char[] c);//每次读取b的长度的字符到数组中,返回读取到的有效字节个数,读取到末尾时,返回 -1
16.4.2 BufferedReader
构造方法:
public BufferedReader(Reader in);//创建一个新的缓冲输入流。
特有方法:
public String readLine();//读一行文字。
16.4.3 InputStreamReader
字符编码和字符集:
ASCII字符集 :1个字节表示一个字符
GBxxx字符集:GB2312,GBK(常用,2个字节表示汉字),GB18030
Unicode字符集:最多使用4个字节来表示字符,包括UTF-8(一种编码格式,3个字节表示汉字),UTF-16,UTF- 32
是Reader的子类,是从字节流到字符流的桥梁
构造方法:
InputStreamReader(InputStream in);//创建一个使用默认字符集的字符流
InputStreamReader(InputStream in, String charsetName);//创建一个指定字符集的字符流。
16.5 Writer(字符输出流)
16.5.1 FileWriter
构造方法:
FileWriter(File file);//创建一个新的 FileWriter,给定要读取的File对象。
FileWriter(String fileName);//创建一个新的 FileWriter,给定要读取的文件的名称。
写出方法:
write(int b);//每次写出一个字符数据
write(char[] c);//写出字节数组中的数据
write(char[] c, int off, int len);//写出指定长度字节数组,每次写出从off索引开始,len个字节
write(String str);//写出字符串
write(String str, int off, int len);//写出字符串
//需要通过flush方法(流对象可以继续使用)或者close方法刷新缓冲区(字符转化为字节 )
16.5.2 BufferedWriter
构造方法:
public BufferedWriter(Writer out);//创建一个新的缓冲输出流。(通过flush或者close方法刷新缓冲区)
特有方法:
public void newLine();//写一行行分隔符,由系统属性定义符号。
16.5.3 OutputStreamWriter
是Writer的子类,是从字节流到字符流的桥梁
构造方法:
OutputStreamWriter(OutputStream in);//创建一个使用默认字符集的字符流。
OutputStreamWriter(OutputStream in, String charsetName);//创建一个指定字符集的字符流。
16.5.4 PrintWriter
构造方法:
public PrintWriter (Writer out);
public PrintWriter(OutputStream out);
16.6 节点流和处理流
节点流:
直接从数据源读写程序,如FileReader,FileWriter
处理流(包装流):
使用修饰器模式对节点流进行包装,主要以增加缓冲的方式增加读写的效率,如BufferedReader,BufferedWriter
16.7 IO模型
BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO (New I/O):同步非阻塞I/O模式,客户端的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理.如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
17 网络编程
17.1 TCP/IP协议
TCP(Transmission Control Protocol,传输控制协议):指能够在多个不同网络间实现信息传输的协议簇,TCP协议和IP协议最具代表性
-
OSI7层参考模型:物链网传会表应 TCP实现:将会表应合并成应用层
-
**TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。**关于TCP/IP和HTTP协议的关系,可以这样理解:“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。”
UDP(User Data Protocol,用户数据报协议):
一个数据包就能够完成数据通信 ,数据包不分段 ,不需要建立会话 ,不需要流量控制 ,属于不可靠传输 , 屏幕广播 、多播 、广播都是基于UDP协议
17.1.1 三次握手四次挥手
SYN 同步位 SYN=1 表示进行一个连接请求
ACK 确认位 ACK=1/0 确认有效/无效
ack 确认号 对方发送序号+1
seq 序号
三次握手:
客户端发送SYN=1,seq=x(随机)
服务端回复SYN=1,ACK=1,ack=x+1,seq=y(随机)
客户端发送ACK=1,ack=y+1,seq=x+1
第三次握手是为了防止失效的连接请求到达服器,让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
四次挥手:
客户端发送FIN=1,表示要断开连接,停止发送数据,seq=u
服务端回复ACK=1,ack=u+1,seq=v,这时称为半关闭状态,服务端还可以发送数据
服务器发送FIN=1,ACK=1,ack=u+1,seq=w
客户端回复ACK=1,ack=w+1,seq=u+1,等待2MSL关闭
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文
17.2 InetAddress
常用方法:
public static InetAddress getLocalHost();//获取本机对象
public static InetAddress getByName();//根据指定主机名/域名获取对象
public String getHostName();//获取InetAddress对象的主机名
public String getHostAddress();//获取InetAddress对象的地址
17.3 TCP编程
17.3.1 Socket
构造方法:
Socket socket = new Socket("127.0.0.1", 6666);
常用方法:
public InputStream getInputStream();//返回此套接字的输入流
public OutputStream getOutputStream();//返回此套接字的输出流
public void close();//关闭此套接字
public void shutdownOutput();//禁用此套接字的输出流
17.3.2 ServerSocket
构造方法:
ServerSocket server = new ServerSocket(6666);
常用方法:
public Socket accept();//监听并接受连接,返回一个新的Socket对象,用于和客户端实现通信
17.4 UDP编程
17.4.1 UDPReceiver
DatagramSocket socket = new DatagramSocket(9999);//创建对象准备在9999端口接收数据
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);//准备一个包对象接收数据
socket.receive(packet);//接收数据填充到packet中
byte[] data = packet.getData();//解包获得实际接收到的数据
socket.close();
17.4.2 UDPSender
DatagramSocket socket = new DatagramSocket(9998);//创建对象准备在9998端口接收数据
byte[] data = "hello".getBytes();
DatagramPacket packet =
new DatagramPacket(data, data.length, InetAddress.getByName("192.168.12.1"), 9999);//将要发送的数据封装,指定IP和端口
socket.send(packet);//发送数据
socket.close();
18 反射
允许程序在运行期间借助反射API取得任何类的内部信息,并可以操作对象的属性和方法
18.1 获取Class对象的方式
1.Class.forName(“全类名”)
将字节码文件加载进内存,返回Class对象,Source源代码阶段
多用于配置文件,将类名定义在配置文件中。读取文件,加载类
2.类名.class
Class类对象阶段
多用于参数的传递
3.对象.getClass()
Runtime运行阶段
多用于对象的获取字节码的方式
注意:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个
18.2 Class对象方法
1.Class对象获取成员变量们:
public Field[] getFields();//获取所有public修饰的成员变量
public Field getField(String name);//获取指定名称的public修饰的成员变量
public Field[] getDeclaredFields();//获取所有的成员变量
public Field getDeclaredField(String name);//获取指定名称的所有成员变量
2.Class对象获取构造方法们:
public Constructor<?>[] getConstructors();
public Constructor<T> getConstructor(Class<?>... parameterTypes);
public Constructor<T> getDeclaredConstructor(类Class<?>... parameterTypes);
public Constructor<?>[] getDeclaredConstructors();
3.Class对象获取成员方法:
public Method[] getMethods();
public Method getMethod(String name, Class<?>... parameterTypes);
public Method[] getDeclaredMethods();
public Method getDeclaredMethod(String name, Class<?>... parameterTypes);
4.获取包名,类名
public Package getPackage();//Class对象.getPackage().getName()获取包名
public String getSimpleName()//类名
public String getName()//完整类名
用例:
Properties pro = new Properties();//创建Properties对象
ClassLoader classLoader = ReflectTest.class.getClassLoader()//创建类加载器对象
InputStream is = classLoader.getResourceAsStream("pro.properties");//获取class目录下的配置文件,参数是配置文件名
pro.load(is);//加载配置文件
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");//获取数据
Class cls = Class.forName(className);//加载该类进内存,返回Class对象
Object obj = cls.newInstance();//创建对象
Method method = cls.getMethod(methodName);//获取方法对象
method.invoke(obj);//执行方法
18.2.1 Field
Field类常用方法:
public void set(Object obj, Object value);//设置值
public get(Object obj);//获取值
public void setAccessible(true)//忽略访问权限修饰符的安全检查,暴力反射,以下同
18.2.2 Constructor
Constructor类常用方法:
public T newInstance(Object... initargs);//创建对象
//如果使用空参数构造方法创建对象,可以直接使用Class对象.newInstance()
18.2.3 Method
Method类常用方法:
Object invoke(Object obj, Object... args);//执行方法
18.3 反射性能优化
1.关闭安全检查
method.m.setAccessible(true);
2.用缓存,比如redis,将反射得到元数据保存起来,使用时,只需从内存中调用即可
18.4 类加载
18.4.1 类加载的时机
静态加载(编译期):
1.创建对象实例时
2.创建子类对象,父类对象也会被加载
3.使用类的静态成员时
动态加载(运行时):
1.反射
18.4.2 类加载的阶段
18.4.2.1 加载
将类的class文件以二进制字节流的方式加载到内存中,并创建一个代表该类的java.lang.Class对象,此过程由类加载器完成
18.4.2.2 连接
1.验证
确保class文件包含的信息符合虚拟机的要求,包括:文件格式验证(是否以魔数 oxcafebabe开头),元数据验证,字节码验证,符号引用验证
2.准备
JVM对在方法区中对静态变量分配内存并进行默认初始化
3.解析
JVM将常量池中的符号引用替换为直接引用
18.4.2.2 初始化
执行<clinit>()方法,由编译器按语句在源文件中出现的顺序,依次自动收集类中所有静态变量的赋值动作和静态代码块中的语句,并进行合并,并保证线程安全
18.5 代理
见设计模式
19 JDK8新特性
19.1 Lambda表达式
1.使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
2.使用Lambda必须具有上下文推断,也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
标准格式:(参数类型 参数名称) ‐> { 代码语句 }
省略规则:
1.小括号内参数的类型可以省略
2.如果小括号内有且仅有一个参,则小括号可以省略
3.如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号
19.2 Stream流
Stream流是一个来自数据源的元素队列
元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源流的来源可以是集合,数组等
获取流:
Collection集合使用stream()默认方法
数组使用Stream.of()静态方法
常用方法:
1.逐一处理(终结方法)
void forEach(Consumer<? super T> action);
Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach(name‐> System.out.println(name));
2.过滤
Stream<T> filter(Predicate<? super T> predicate);
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s ‐> s.startsWith("张"));
3.映射
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Stream<String> original = Stream.of("10", "12", "18");
Stream<Integer> result = original.map(str‐>Integer.parseInt(str));
4.统计个数(终结方法)
long count();
5.取用前几个
Stream<T> limit(long maxSize);
6.跳过前几个
Stream<T> skip(long n);
7.组合
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
//将数组转换为集合:
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
19.3 方法引用
双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。
如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代
Lambda表达式写法:s -> System.out.println(s); (参数通过Lambda传递给System.out.println方法)
方法引用写法:System.out::println (直接让System.out中的println方法来取代Lambda)
注意:
Lambda中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常
通过对象名引用成员方法
如果一个类中已经存在了一个成员方法,可以用这个方法替换需要传递的lambda
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
通过类名称引用静态方法
Math::abs等效于n -> Math.abs(n)
通过super/this引用成员方法
super::sayHello this::buyHouse
类的构造器引用
Person::new等效于name -> new Person(name)
数组的构造器引用
int[]::new等效于length -> new int[length]
20 log4j
#log4j.rootCategory=[level],appenderName1,appenderName2
#level建议从高到低:error,warn,info,debug appenderName指定输出器名字
log4j.rootCategory=debug,stdout
#设置输出到控制台,使用灵活布局,自定义布局格式
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n