Java基础知识回顾
文章目录
- Java背景知识
- Java SE基础
- 基础语法
- JVM 内存模型
- 方法重载
- 面向对象
- 基础
- 执行原理
- 注意要点
- 面向对象的三大特征
- 封装
- 继承
- 多态
- 基础API
- String
- 创建
- 注意要点
- ArrayList
- 创建
- 常用方法
- Java SE进阶
- static 关键字
- 基础
- 拓展
- 代码块
- final 关键字
- 常量
- 继承
- 单继承
- 方法重写
- 子类访问、构造的特点
- 抽象类
- 接口
- 接口的继承和实现注意事项
- 内部类
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
- 枚举类
- 常见应用场景 以及优势
- 泛型
- 泛型类
- 泛型接口
- 泛型方法
- 泛型的上下限和通配符
- 设计模式
- 单例设计模式
- 饿汉式单例
- 懒汉式单例
- 模板方法设计模式
- 实现
- 常用API
- Object
- Objects
- 包装类
- 和基本类型转化
- 包装类的其他常见操作
- StringBuilder、StringBuffer 与StringJoiner
- StringBuilder
- StringBuffer
- StringJoiner
- Math、System、Runtime
- Math
- System
- Runtime
- BigDecimal
- 注意要点
- 常用方法
- 舍入模式
- 日期相关
- JDK8之前的
- Date
- 常用方法
- SimpleDateFormat
- 常用方法
- Calendar
- 常用方法
- 注意事项
- JDK8之后的
- 代替Calendar
- LocalDate
- LocalTime
- LocalDateTime
- ZoneId:时区
- ZonedDateTime:带时区的时间
- 代替Date
- Instant:时间戳/时间线
- 代替SimpleDateFormat
- DateTimeFormatter:用于时间的格式化和解析
- 其他补充
- Period:时间间隔(年,月,日)
- Duration:时间间隔(时、分、秒,纳秒)
- Arrays类
- 排序相关
- JDK8新特性
- Lambda表达式
- Lambda表达式的省略规则
- 方法引用
- 静态方法的引用
- 实例方法的引用
- 特定类型方法的引用
- 构造器引用(没啥应用)
- JDK8 简化案例
- 背景
- Lambda 简化
- 方法引用简化
- 其他案例
- 编译器工作原理
- Stream
- Stream流的使用步骤
- 获取流的方法
- Stream流的中间方法
- 终结方法
- 注意
- 正则表达式
- 书写规则
- 字符类
- 预定义字符(只匹配单个字符)
- 数量词
- 其他
- 可变参数
- 注意事项
- 异常
- 异常体系
- 处理异常
- 自定义异常
- 异常的作用
- 异常的常见处理方式
- 集合进阶
- Collection
- Collection集合特点
- Collection的常用方法
- Collection的遍历方式
- 迭代器
- 增强for循环
- lambda表达式
- List
- 个性方法
- 遍历方法
- ArrayList
- 特点
- 底层原理
- 适合场景
- 不适合场景
- LinkedList
- 特点
- 特有方法
- 应用场景
- Set
- 特点
- 实现类的特点
- 特有功能
- HashSet
- 底层原理
- 特点
- 哈希表去重
- LinkedHashSet
- 底层原理
- TreeSet
- 底层原理
- 自定义排序
- 总结
- 集合的并发修改异常
- 内容
- 底层原理
- 工具类
- 常用方法
- Map
- Map集合体系
- 常用方法
- Map的遍历方式
- 键找值
- 键值对
- forEach+Lambda
- Map的具体实现类
- 多线程
- 实现多线程的方法
- 继承Thread类
- 注意
- 实现Runnable接口
- 实现Callable接口,并封装成FutureTask类
- 线程的常用方法
- 线程安全
- 线程同步
- 同步代码块
- 同步方法
- Lock锁
- 线程通信
- Condition
- 线程生命周期
- 并发和并行
- 线程池
- 得到线程池
- 常用方法
- 常用拒绝策略
- 注意事项
- 线程池使用
- 使用工具类Executors
- File、IO流
- File类
- 绝对路径、相对路径
- File创建对象
- **File提供的功能**
- **创建文件**
- **删除文件**
- 遍历文件夹
- 注意
- 前置知识
- 递归
- 字符集
- ASCII字符集
- GBK
- Unicode字符集
- UTF-32
- UTF-8
- 注意
- Java进行字符编码、解码
- IO流
- IO流的类型
- IO流的体系
- 字节流
- FilelnputStream
- 注意
- FileOutputStream
- 适合的应用场景
- 字符流
- FileReader输入
- FileWriter(文件字符输出流)
- 注意
- 缓冲流
- 字节缓冲流
- 字符缓冲流
- 输入
- 输出
- 其他流
- 转换流
- 输入流
- 输出流
- 打印流
- 注意
- 应用
- 数据流
- 序列化流
- ObjectInputStream
- ObjectOutputstream
- 注意
- 释放资源
- try - catch -finally
- try - with - resources
- 总结
- 特殊文件
- 属性文件
- XML文件
- XML语法
- 使用XML读写
- 框架
- IO框架
- Commons-io
- 日志技术
- 网络编程
- 通信三要素
- InetAddress
- UDP通信
- TCP通信
- 案例
- 单元测试
- 反射、注解、动态代理
- 反射
- 获取字节码:Class对象
- 获取构造器:Constructor对象
- 获取成员变量:Field对象
- 获取成员方法:Method对象
- 注解
- 自定义注解
- 注解的原理
- 元注解
- @Target
- @Retention
- 注解的解析
- 应用场景
- 动态代理
Java背景知识
- Java SE和Java EE的区别
SE是Java的核心和基础,EE是企业级应用开发的一套解决方案,
除此之外还有Java Me 是小型化的Java
- 普通Java的运行过程:
运行Javac **.java 将会生成**.class文件 之后执行 java **将会运行对应的代码。
不过这样编写需要文件名和类名保持一致,编写的内容需要有main方法
从jdk11开始支持直接 java **.java来完成以上过程
- Java的跨平台性
跨平台中的平台指的是操作系统,Java语言的跨平台性是指Java程序可以在不同的操作系统上运行。前提是相应的操作系统安装了对应的JVM。
Java实现了一次编译处处可用,将编译后的class文件放到不同操作系统的JVM中依然可以正常使用
-
jdk的组成
Java SE基础
基础语法
概念:
- 标志符:标志符就是名字,我们写程序时会起一些名字,如类名、变量名等等都是标识符。由数字、字母、下划线(_)和美元符($)等组成。不能数字开头,不能使用关键字。
记忆要点:
- 注释:文档注释是可以提取到一个程序说明文档当中的
- 表达式:表达式的自动转换由参与运算的最大的类型决定,char byte short 直接转int
- 运算:运算时,从左到右,能算则算,不能就转str连接
- 扩展赋值运算:+=、%=等自带强制类型转化
- 强制类型转化:强制类型转换可能造成数据丢失,对于浮点数是直接截断小数保留整数的
- 逻辑运算:双 与 运算以及双 或 运算和单 的区别在于 双的处理逻辑是短路的,左边出结果后右边可能就不需要运算了
- 输入:JAVA使用 scanner进行键盘输入,Scanner sc= new Scanner(System.in)、sc.next()
- 进制:Java程序中支持书写二进制、八进制、十六进制的数据,分别需要以0B或者0b、0、0X或者0x开头。
- 引用数据类型的默认值为null
- 数组初始化:静态:int[] a = {1,1,1} 动态:int[] a= new int[3]
- 参数传递:基本类型和引用类型传递的都是值,但是引用传递的是存储的地址
权限修饰符用于限制类当中的成员可以被访问的范围
修饰符 | 在本类中 | 同一个包下的其他类里 | 任意包下的子类里 | 任意包下的任意类里 |
---|---|---|---|---|
private | 1 | |||
缺省(不写) | 1 | 1 | ||
protected | 1 | 1 | 1 | |
public | 1 | 1 | 1 | 1 |
JVM 内存模型
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。其中,线程共享的数据区包括方法区和堆,线程私有的数据区包括虚拟机栈、本地方法栈和程序计数器。
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :对于每个线程会单独创建一个运行时栈。对于每个函数调用都会在栈内存生成一个栈帧,所有的局部变量都在栈内存中创建。
- 虚拟机栈:每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。
栈帧被分为三个子实体:局部变量数组、操作数栈、帧数据
- 局部变量数组:包含多少个与方法相关的局部变量并且相应的值业内存储于此
- 操作数栈:若需要执行任何中间操作,操作数栈作为运行时工作区去执行命令
- 帧数据:方法的所有符号都保存在这。在任意异常的情况下,catch块的信息会保存在帧数据里面。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的对象实例,几乎所有的对象实例(和数组)都被存储在这。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
方法重载
方法重载是指一个类中,多个方法的名称相同,但是参数列表不同
一个类中,只要一些方法的名称相同、形参列表不同,那么它们就是方法重载了,其它的都不管
(如:修饰符,返回值类型是否一样都无所谓)。
形参列表不同指的是:形参的个数、类型、顺序不同,不关心形参的名称。
可以对开发中同一类业务实现不同的功能实现。
面向对象
基础
对象是类的实例,类是对象的模板。
类本质上是一种特殊的数据结构,对象是这个数据结构实现的实例。
对象是用类new出来的,有了类就可以创建出对象。
public class 类名 {1、变量,用来说明对象可以处理什么数据2、方法,描述对象有什么功能,也就是可以对数据进行什么样的处理
}
谁的数据谁处理
执行原理
1、将student类、类变量、类方法加载到方法区;
2、new 一个student实例的时候,在堆内存中创建一个student类的实例,包含一个堆的地址指向该student实例存储的地址。以及一个student类的地址指向student类的信息在方法区的地址。
3、并根据参数决定使用的构造器(默认无参构造器),根据构造器的内容执行初始化。
4、 在栈内存中开辟一个student变量接受堆内存的存储地址。
5、当没有任何变量指向该堆内存的地址时,会被判定为垃圾,有Java的自动垃圾回收机制清除掉垃圾对象
注意要点
一个代码文件中可以写多个class类,但是只能有一个用public修饰,且必须是文件名
this就是一个变量,可以在方法中拿到当前的对象。其执行原理如下:
- 栈内存中的student 通过地址找到堆内存中的实例对象
- 在堆内存中的对象通过类的地址找到方法区的printThis()方法,同时将自己的堆地址传给方法区的this进行接收
- 实际上编译器会自动对方法加入一个类型为该类的this参数
this主要用来解决变量名称冲突问题。
如果定义了有参构造器,默认的无参构造器将不会自动生成,如果需要则应该手动实现
构造器:
- 方法名与类名相同,大小写也必须保持一致
- 没有返回值类型,连void都没有
- 不能通过return语句带回结果数据
成员变量和局部变量的区别在于:
区别 | 成员变量 | 局部变量 |
---|---|---|
类中位置不同 | 类中,方法外 | 常见于方法中 |
初始化值不同 | 有默认值,不需要初始化赋值 | 没有默认值,使用之前必须完成赋值 |
内存位置不同 | 堆内存 | 栈内存 |
作用域不同 | 整个对象 | 在所归属的大括号中 |
生命周期不同 | 与对象共存亡 | 随着方法的调用而生,随着方法的运行结束而亡 |
面向对象的三大特征
封装
封装就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理这些数据的方法,设计到一个对象中去。
设计规范是:合理隐藏(private)、合理暴露(public)
一个例子是:对成员变量设置为private,但是生成set和get方法进行赋值和调用。在对应的setter和getter可以编写一些逻辑限制访问。
如果:
- 一个类中的成员变量都私有,且都对外提供相应的getXxx,setXxx方法
- 类中有一个公共的无参的构造器。
那么他就是一个实体类(JavaBean)
实体类只负责数据存取,而对数据的处理交给其他类来完成,以实现数据和数据业务处理相分离。
继承
Java中提供了一个关键字extends,用这个关键字,可以让一个类和另一个类建立起父子关系。
子类能继承父类的非私有成员(成员变量、成员方法)
子类的对象是由子类、父类共同完成的。
其实现的原理如下:
main方法在执行到B b = new B();的时候将B加载到方法区,之后因为Extend A将 A也加载到方法区。依据A和B创建在堆内存中需要存放的对象内容。栈内存存放的是堆内存的地址,在堆当中根据类的地址先找到B之后根据继承关系找到A。
更多内容,查看JavaSE进阶当中的继承部分
多态
多态是 继承/实现 下的一种现象,表现为:对象多态、行为多态。Java只有行为多态。
因此Java多态的前提如下:
- 有继承/实现关系
- 存在父类引用子类对象
- 存在方法重写
可以用: 子类 变量名 = (子类) 父类变量 这样进行强制类型转换
但是在强制转换前Java建议使用instanceof 关键字判断当前对象的真实类型
Person k ;
if(k instanceof Student){Student s = (Student) k;
}else if(k instanceof Teacher){Teacher t = (Teacher) k;
}
基础API
String
创建
- 方式一:Java程序中的所有字符串文字(例如“abc”)都为此类的对象。
String name="小黑";
String schoolName="黑马程序员";
- 方式二:调用String类的构造器初始化字符串对象。
| 构造器 | 说明 |
| — | — |
| public String() | 创建一个空白字符串对象,不含有任何内容 |
| public String(String original) | 根据传入的字符串内容,来创建字符串对象 |
| public String(char[] chars) | 根据字符数组的内容,来创建字符串对象 |
| public String(byte[] bytes) | 根据字节数组的内容,来创建字符串对象 |
注意要点
- String的对象是不可变字符串对象
每次试图改变字符串对象实际上是新产生了新的字符串对象了,变量每次都是指向了新的字符串对象,之前字符串对象的内容确实是没有改变的,因此说String的对象是不可变的。
- 字符串在堆中的存储
只要是以“…"方式写出的字符串对象,会在堆内存中的字符串常量池中存储。但通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
如果在创建常量对象的时候常量池中已经有一份相同的常量,那么会直接使用对应的常量地址而不是创建一个新的。但是如果是new的不管有没有都在其他位置创建一个新的。
- 编译与运算
对于相关的运算而言,运算是在堆当中的,不能直接在常量池当中进行存取。
在编译器运行过程中预见连续的字面量运算,会直接进行编译器优化进行相关运算后将结果作为class字节码出现。
ArrayList
创建
ArrayList list = new ArrayList();
如此创建的是包含任意类型对象的,甚至可以存放不同的类型。如果想要限制类型应该
ArrayList<String> list = new ArrayList<String>();
或者在jdk7之后更支持下面这种
ArrayList<String> list = new ArrayList<>();
常用方法
常用方法名 | 说明 |
---|---|
public boolean add(E e) | 将指定的元素添加到此集合的末尾 |
public void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
public E get(int index) | 返回指定索引处的元素 |
public int size() | 返回集合中的元素的个数 |
public E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
public boolean remove(Object o) | 删除指定的元素,返回删除是否成功 |
public E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
Java SE进阶
static 关键字
基础
static修饰成员变量就会变成 类变量(静态成员变量),不修饰的就是实例变量。类变量在内存中就只有一份,与类一起加载一次,被所有对象共享,可以通过类名.类变量进行访问。其执行原理如下:
方法区加载Student方法时发现有类对象,就会把对应的对象加载到堆内存当中。如果使用实例对象访问类对象,那么会根据类的地址找到方法区的class,之后找到对应的类方法在堆当中的地址
修饰方法就是类方法(静态方法),不修饰就是实例方法
类方法的常见应用场景是做工具类:
- 工具类中的方法都是一些类方法,每个方法都是用来完成一个功能的,工具类是给开发人员共同使用的。
- 工具类没有创建对象的需求,建议将工具类的构造器进行私有。
- 使用工具类不用创建实例,直接使用类名调用即可,节省内存。
注意事项
- 类方法中可以直接访问类的成员,不可以直接访问实例成员。
- 实例方法中既可以直接访问类成员,也可以直接访问实例成员。
- 实例方法中可以出现this关键字,类方法中不可以出现this关键字的。
static关键字还可以修饰代码块、内部方法,变成静态代码块、静态内部方法
拓展
代码块
代码块是类的5大成分之一(成员变量、构造器、方法、代码块、内部类)。
- 静态代码块:
- 格式:static{}
- 特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
- 作用:完成类的初始化,例如:对类变量的初始化赋值。
- 实例代码块:
- 格式:{}
- 特点:每次创建对象时,执行实例代码块,并在构造器前执行。
- 作用:和构造器一样,都是用来完成对象的初始化的,例如:对实例变量进行初始化赋值。
final 关键字
final关键字是最终的意思,可以修饰(类、方法、变量)
- 修饰类:该类被称为最终类,特点是不能被继承了。
- 修饰方法:该方法被称为最终方法,特点是不能被重写了。
- 修饰变量:该变量只能被赋值一次。
注意要点:
- final修饰基本类型的变量,变量存储的数据不能被改变。
- final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。
常量
使用了 static final 修饰的就变成了常量,常量在程序编译后会被宏替换成字面量
继承
继承的基础部分在JavaSE/面向对象/继承 当中讲过了
单继承
Java是单继承的,Java中的类不支持多继承,但是支持多层继承。
Object类是Java中所有类的祖宗。没有写extend的花默认会添加Extend Object
方法重写
子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问,Java会遵循就近原则。
重写小技巧:使用Override注解,他可以指定java编译器,检查我们方法重写的格式是否正确,代码可读性也会更好。
-
子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限(public>protected>缺省)。
-
重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小。
-
私有方法、静态方法不能被重写,如果重写会报错的。
类重写Object类的toString()方法,以便返回对象的内容。这样使用sout输出的时候就能直接输出内容,而不是地址了
子类访问、构造的特点
就近访问变量和方法
默认先调用父类的无参构造器即:构造器中省略了super();
所以,若父类没有无参构造器,应该手写调用有参构造器。
子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先初始化赋值,再回来把对象里包含子类这部分的数据也进行初始化赋值。
子类还可以使用this(…)来调用该类的其他构造器。
注意this()和super()不能同时出现,且都需要放在第一行
抽象类
抽象类和抽象方法都是用abstract修饰的;
抽象方法只有方法签名,不能写方法体。
抽象类中可以不写抽象方法,但有抽象方法的类一定是抽象类
类有的成员(成员变量、方法、构造器)抽象类都具备。
抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
接口
在jdk8之前,接口中只能包含两种内容:1、成员变量 2、成员方法
其中的成员变量默认其实是由public static final 修饰的常量
而成员方法其实是由public abstract修饰的抽象方法
接口可以继承多个其他的接口
public interface A extends B,C,D{}
一个类可以实现多个接口,实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。
使用接口编程的好处:
- 可以弥补Java单继承的不足,一个类可以实现多个接口
- 让程序可以面向接口编程,这样程序员可以灵活的切换多种业务实现方式
接口从jdk8开始新增的三种方法:除了私有方法他们都会默认被public修饰。
- 默认方法:使用default修饰,使用实现类的对象调用。
- 静态方法:static修饰,必须用当前接口名调用
- 私有方法:private修饰,jdk9开始才有的,只能在接口内部被调用。
例子如下:
public interface A {/*** 1. 默认方法:必须使用 default 修饰,默认会被 public 修饰* 实例方法:对象的方法,必须使用实现类的对象来访问。*/default void test1() {System.out.println("==默认方法==");test2(); // 调用接口中的私有方法}/*** 2. 私有方法:必须使用 private 修饰(JDK 9 开始支持)* 实例方法:对象方法,仅供 default 方法调用*/private void test2() {System.out.println("==私有方法==");}/*** 3. 静态方法:必须使用 static 修饰,默认会被 public 修饰* 可使用接口名直接调用 A.test3()*/static void test3() {System.out.println("==静态方法==");}
}
接口的继承和实现注意事项
1、一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承。
2、一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现。
3、一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的。
4、一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
内部类
在编译一个类的时候,假如该类中有内部类,则会直接生成 类$内部类.class 文件
成员内部类
类当中的普通成员
public class Outer{int oa;public class inner{int ia;}
}
JDK16之前不允许成员内部类定义静态成员。
创建对象:
Outer.Inner in =new Outer().new Inner();
访问成员:
可以访问到外部类的实例成员、静态成员
访问内部和外部的方法如下
this.ia
Outer.this.oa
静态内部类
有static修饰的内部类,属于外部类自己持有。
public class Outer{int oa;public static class inner{int ia;}
}
public class Outer{
// 静态内部类
public static class Inner{
创建对象的格式:
Outer.Inner in = new Outer.Inner();
可以直接访问外部类的静态成员,不可以直接访问外部类的实例成员。(类似于一个正常的类方法)
局部内部类
局部内部类是定义在在方法中、代码块中、构造器等执行体中。
比较鸡肋
匿名内部类
就是一种特殊的局部内部类;所谓匿名:指的是程序员不需要为这个类声明名字。
new Animal(){//抽象类或者接口@Override//重写抽象方法public void cry(){}
};//最后会返回一个对应的类的实例对象
特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
作用:用于更方便的创建一个子类对象。
使用场景:常常作为一个参数传递给方法(见实践部分)
枚举类
枚举类的形式:
public enum A{X ,Y ,Z; //名称String name;// 其他成员int value;
}
反编译的结果:
public final class A extends java.lang.Enum<A>{public static final A X = new A();public static final A Y = new A();public static final A Z = new A();public static A[] values();public static A valueof(java.lang.String);
}
注意要点:
- 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
- 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
- 枚举都是最终类,不可以被继承。
- 枚举类中,从第二行开始,可以定义类的其他各种成员。
- 编译器为枚举类新增了几个方法,并且枚举类都是继承:java.lang.Enum类的,从enum类也会继承到一些方法。(e.g. ordinal() 获取在枚举当中的位置)
常见应用场景 以及优势
用来表示一组信息,然后作为参数进行传输。
枚举类的优势:
- 增强代码可读性
- 传递参数错误
- 去除equals两者判断
- 由于常量值地址唯一,使用枚举可以直接通过“==”进行两个值之间的对比,性能会有所提高。
- 编译优势(与常量类相比)
- 常量类编译时,常量被直接编译进二进制代码中,常量值在升级中变化后,需要重新编译引用常量的类,因为二进制代码中存放的是旧值。枚举类编译时,没有把常量值编译到代码中,即使常量值发生改变,也不会影响引用常量的类。
- 修改优势(与常量类相比)
- 枚举类编译后默认final class,不允许继承可防止被子类修改。常量类可被继承修改、增加字段等,易导致父类不兼容。
- 枚举型可直接与数据库交互。
- Switch语句优势
- 使用int、String类型switch时,当出现参数不确定的情况,偶尔会出现越界的现象,这样我们就需要做容错操作(if条件筛选等),使用枚举,编译期间限定类型,不允许发生越界
泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>),称为泛型类、泛型接口,泛型方法、它们统称为泛型。
public class ArrayList<E>{
}
作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!这样可以避免强制类型转换,及其可能出现的异常。
泛型的本质:把具体的数据类型作为参数传给类型变量。
通过反编译可知,实际上在运行的时候是通过Object类运行的。
注意事项:
- 泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除。
- 泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。
泛型类
代码形式如下:
public class ArrayList<E>{
}
泛型接口
代码形式如下:
public interface name<E>{
}
泛型方法
只有自己 声明 了泛型的才叫泛型方法,对于在泛型类当中使用了泛型作为返回值的不叫泛型方法,具体如下:
public static <T> void test(T t){
}
这是泛型方法
public E get(int index){return (E) arr[index];
}
这不是泛型方法
泛型的上下限和通配符
在使用泛型的时候可以自己不定义,而是使用通配符 ? 来表示泛型,比如:
public static void go(ArrayList<? extends Car>cars){
}
其中extends 约束了他的上限 表示传入的这个 ? 的类型必须继承了Car父类或是其祖宗是Car
同时你不能够往一个使用了? extends的数据结构里写入任何的值。
因为符合? extends 的类型很多,我们不确定其中究竟是哪个。苹果香蕉都是水果,声明了ArrayList<苹果> a; 可以传入void get<? extends 水果>();但是不能再其中添加香蕉,因为其实存的是苹果;
设计模式
什么是设计模式
- 一个问题通常有种解法,其中肯定有一种解法是最优的,这个最优的解法被人总结出来了,称之为设计模式。
- 设计模式有20多种,对应20多种软件开发中会遇到的问题。
单例设计模式
确保一个类只有一个对象。其实现方式有很多种。对于有些情况只需要一个对象,比如运行时环境和任务管理器
饿汉式单例
拿对象时,对象早就创建好了,其实现如下:
写法:
-
把类的构造器私有。
-
定义一个类变量记住类的一个对象。
-
定义一个类方法,返回对象。
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 getInstance(){if(b==null){b = new B();}return b;}
}
枚举类设计单例
枚举类当中只有一个对象,那就是单例了
模板方法设计模式
用于解决方法中存在重复代码的问题
实现
- 定义一个抽象类
- 定义两个方法:
- 模板方法:方相同的代码,建议使用final修饰,这样模板不能修改
- 抽象方法:具体实现交给子类
// 抽象类
public abstract class AbstractClass {// 模板方法,提供公共的调用顺序,不允许子类override,所以设置为finalpublic final void templateMethod() {specificMethod();abstractMethod1();abstractMethod2();hookMethod();}// 钩子方法,子类可根据实际情况选择是否要试下该方法public void hookMethod() {}// 具体方法,设置为private,避免子类override,并且不允许外部访问private void specificMethod() {System.out.println("抽象类中的具体方法被调用...");}// 抽象方法1public abstract void abstractMethod1();// 抽象方法2public abstract void abstractMethod2();
}
常用API
Object
Object类的常见方法 | 说明 |
---|---|
方法名 public String toString() | 返回对象的字符串表示形式。 |
public boolean equals(Object o) | 判断两个对象是否相等。 |
protected Object clone() | 对象克隆 |
这里的equals默认是比较地址的,实际上我们需要重写,在Idea中有快捷的重新方式
clone() 当某个对象调用这个方法时,这个方法会复制一个一模一样的新对象返回。
Object的clone是protected 的要使用的话,需要对应的子类重写了这个clone方法,并且要继承cloneable接口才行。
直接使用的话是浅克隆,对象如果使用了引用对象,那么拷贝的仅仅是地址,要实现深克隆需要对引用对象生成一个新对象。
深克隆的实现需要重写clone方法时,先super().clone()然后对 引用对象实现 引用对象.clone()
Objects
Objects是一个工具类,提供了很多操作对象的静态方法给我们使用。
方法名 | 说明 |
---|---|
public static boolean equals(Object a,Object b) | 先做非空判断,再比较两个对象 |
public static boolean isNull(Object obj) | 判断对象是否为null,为null返回true,反之 |
public static boolean nonNull(Object obj) | 判断对象是否不为null,不为null则返回true,反之 |
包装类
包装类就是把基本类型的数据包装成对象。
基本数据类型首字母大写、int => Interger 、char => Character
泛型和集合不支持基本数据类型,只支持引用数据类型。
和基本类型转化
最重要的就是自动装箱和自动拆箱;
自动装箱可以自动把基本类型的数据转化成对象;
而自动拆箱可以自动把包装类型的对象转化成对应的基本数据类型
Interger a = new Interger(12);Interger a = Interger.valueOf(12);Interger a = 12;int b = a;
包装类的其他常见操作
- 可以把基本类型的数据转换成字符串类型。
public static String toString(double d)public String toString()
但是其实
1+“”
也可以实现相同的功能
- 可以把字符串类型的数值转换成数值本身对应的数据类型。
public static int parselnt(String s)public static Integer valueOf(String s)
其中的valueOf其实是更推荐的
示例:
Interger a = 29;
String agestr = Interger.toString(a);
int ageI = Integer.valveOf(agestr);//29
StringBuilder、StringBuffer 与StringJoiner
StringBuilder
StringBuilder代表可变字符串对象,相当于是一个容器,它里面装的字符串是可以改变的,就是用来操作字符串的。
好处:StringBuilder比String更适合做字符串的修改操作,效率会更高,代码也会更简洁。
常用方法:
构造器 | 说明 |
---|---|
public StringBuilder() | 创建一个空白的可变的字符串对象,不包含任何内容 |
public StringBuilder(String str) | 创建一个指定字符串内容的可变字符串对象 |
方法名称 | 说明 |
---|---|
public StringBuilder append(任意类型) | 添加数据并返回stringBuilder对象本身 |
public StringBuilder reverse() | 将对象的内容反转 |
public int length() | 返回对象内容长度 |
public String toString() | 通过toString()就可以实现把stringBuilder转换为String |
StringBuffer
StringBuffer 和StringBuilder的用法是完全一样的,唯一不同的在于Builder是线程不安全的 Buffer是线程安全的
StringJoiner
JDK8开始才有的,跟StringBuilder-一样,也是用来操作字符串的,也可以看成是一个容器,创建之后里面的内容是可变的。
好处:不仅能提高字符串的操作效率,并且在有些场景下使用它操作字符串,代码会更简洁
构造器 | 说明 |
---|---|
public StringJoiner (间隔符号) | 创建一个stringJoiner对象,指定拼接时的间隔符号 |
public StringJoiner(间隔符号,开始符号,结束符号) | 创建一个stringJoiner对象,指定拼接时的间隔符号、开始符号、结束符号 |
方法名称 | 说明 |
---|---|
public StringJoiner add (添加的内容) | 添加数据,并返回对象本身 |
public int length() | 返回长度 (字符出现的个数) |
public String toString() | 返回一个字符串 (该字符串就是拼接之后的结果) |
Math、System、Runtime
Math
方法名 | 说明 |
---|---|
public static int abs(int a) | 获取参数绝对值 |
public static double ceil(double a) | 向上取整 |
public static double floor(double a) | 向下取整 |
public static int round(float a) | 四舍五入 |
public static int max(int a,int b) | 获取两个int值中的较大值 |
public static double pow(double a,double b) | 返回a的b次幂的值 |
public static double random() | 返回值为double的随机值,范围[0.0,1.0) |
System
方法名 | 说明 |
---|---|
public static void exit(int status) | 终止当前运行的Java虚拟机。 |
public static long currentTimeMillis() | 返回当前系统的时间毫秒值形式 |
System 的exit实际上是调用Runtime的exit方法实现的。
Runtime
代表程序所在的运行环境。Runtime是一个单例类。
方法名 | 说明 |
---|---|
public static Runtime getRuntime() | 返回与当前Java应用程序关联的运行时对象 |
public void exit(int status) | 终止当前运行的虚拟机 |
public int availableProcessors() | 返回Java虚拟机可用的处理器数。 |
public long totalMemory() | 返回Java虚拟机中的内存总量 |
public long freeMemory() | 返回Java虚拟机中的可用内存 |
public Process exec(String command) | 启动某个程序,并返回代表该程序的对象 |
BigDecimal
用于解决浮点型运算时,出现结果失真的问题。
注意要点
-
最好使用传入字符串的构造器 或者他提供的ValueOf静态方法来 转化成BigDecimal、不然还是会有精度问题
-
BigDecimal提供的是精确的运算和结果,诸如1.0/3.0这种运算是会抛出异常的,应该设置精确到几位:d1.divide(d2 , 2 , RoundingMode.HALF_UP);
常用方法
方法名 | 说明 |
---|---|
public BigDecimal(double val) | 得到的BigDecimal对象是无法精确计算浮点型数据的。注意:不推荐使用这个 |
public BigDecimal(String val) | 得到的BigDecimal对象是可以精确计算浮点型数据的。可以使用。 |
public static BigDecimal value0f(double val): | 通过这个静态方法得到的BigDecimal对象是可以精确运算的。是最好的方案。 |
方法名 | 说明 |
---|---|
public BigDecimal add(BigDecimal augend) | 加法 |
public BigDecimal subtract(BigDecimal augend) | 减法 |
public BigDecimal multiply(BigDecimal augend) | 乘法 |
public BigDecimal divide(BigDecimal b) | 除法 |
public BigDecimal divide(另一个BigDecimal对象,精确几位,舍入模式) | 除法,精确到几位 |
public double doubleValue() | 把BigDecimal对象又转换成double类型的数据。 |
舍入模式
有3种方式
1、运算时传入MathContext 对象
- public MathContext(int setPrecision, RoundingMode setRoundingMode):传入所占位数和舍入模式
- new MathContext(13, RoundingMode.FLOOR);
2、运算后调用setScale方法
- b.setScale(3, RoundingMode.HALF_DOWN)
3、使用Math当中的round进行舍入
日期相关
JDK8之前的
Date
常用方法
构造器 | 说明 |
---|---|
public Date() | 创建一个Date对象,代表的是系统当前此刻日期时间。 |
public Date(long time) | 把时间毫秒值转换成Date日期对象。(距离1970年1月1日的距离时间) |
常见方法 | 说明 |
---|---|
public long getTime() | 返回从1970年1月1日 00:00:00走到此刻的总的毫秒数 |
public void setTime(long time) | 设置日期对象的时间为当前时间毫秒值对应的时间 |
SimpleDateFormat
代表简单日期格式化,可以用来把日期对象、时间毫秒值格式化成我们想要的形式。
常用方法
常见构造器 | 说明 |
---|---|
public SimpleDateFormat(String pattern) | 创建简单日期格式化对象,并封装时间的格式 |
格式化时间的方法 | 说明 |
---|---|
public final String format(Date date) | 将日期格式化成日期/时间字符串 |
public final String format(object time) | 将时间毫秒值式化成日期/时间字符串 |
public Date parse(String source) | 把字符串时间解析成日期对象 |
其中在使用parse解析时间的时候,设置的格式一定要和将要解析的时间格式是一模一样的。
Calendar
代表的是系统此刻时间对应的日历。
通过它可以单独获取、修改时间中的年、月、日、时、分、秒等。
常用方法
方法名 | 说明 |
---|---|
public static Calendar getInstance() | 获取当前日历对象 |
public int get(int field) | 获取日历中的某个信息。 |
public final Date getTime() | 获取日期对象。 |
public long getTimeInMillis() | 获取时间毫秒值 |
public void set(int field,int value) | 修改日历的某个信息。 |
public void add(int field,int amount) | 为某个信息增加/减少指定的值 |
注意事项
- Calendar是一个抽象类,他的一个直接子类是Gregoriancalendar
- 日历当中的月份是从0开始的
- 参数当中的field都是采用常量的形式给出的,Calendar.DAT_OF_YEAR 等
- calendar是可变对象,一旦修改后其对象本身表示的时间将产生变化。
JDK8之后的
JDK8开始之后新增的时间API优点如下:
- 设计更合理,功能丰富,使用更
方便。 - 都是不可变对象,修改后会返回新
的时间对象,不会丢失最开始的时间。 - 线程安全。
- 能精确到毫秒、纳秒。
代替Calendar
LocalDate:代表本地日期(年、月、日、星期)
LocalTime:代表本地时间(时、分、秒、纳秒)
LocalDateTime:代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒)
他们获取对象的方法:
方法名 | 示例 |
---|---|
LocaDate ld = LocalDate.now(); | |
public static Xxxx now(): 获取系统当前时间对应的该对象 | LocalTime lt = LocalTime.now(); |
LocalDateTime ldt = LocalDateTime.now(); |
LocalDate
这个对象是不可变对象,修改完之后返回的是一个全新的对象。
//0、获取本地日期对象(不可变对象)
LocalDate ld=LocalDate.now();//年月日
//1、获取日期对象中的信息
int year = ld.getYear();/
int month = ld.getMonthValue();//(1-12)
int day = ld.getDayofMonth();/
int dayofYear=ld.getDayofYear();//一年中的第几天
int dayofWeek=ld.getDayofWeek().getValue();//星期几
//2、直接修改某个信息:withYear、withMonth、withDayofMonth、withDayofYear
LocalDate ld2 = ld.withYear(2099);
LocalDate ld3 = ld.withMonth(12);
//3、把某个信息加多少:plusYears、plusMonths、plusDays、plusWeeks
LocalDate ld4 = ld.plusYears(2);
LocalDate ld5 = ld.plusMonths(2);
//4、把某个信息减多少:minusYears、minusMonths、minusDays、minusWeeks
LocalDate ld6 = ld.minusYears(2);
LocalDate ld7 = ld.minusMonths(2);
//5、获取指定日期的ocalDate对象:public static LocalDate of(int year,
LocalDate ld8 = LocalDate.of(2099,12,12);
LocalDate ld8 = LocalDate.of(2099,12,12);
//6、判断2个日期对象,是否相等,在前还是在后:equals isBefore isAfter
System.out.println(ld8.equals(ld9));//true
System.out.println(ld8.isAfter(ld));//true
System.out.println(ld8.isBefore(ld));//false
LocalTime
LocalTime是用于代表本地时间(时、分、秒、纳秒)的
LocalTime的用法和LocalDate差不多,now得到对象,getHour()等得到信息,withHour()等直接修改,plusHour()等加,minusHour()等减,of()直接得到对应时间,equals、isBefore、isAfter 比较
修改后的对象是一个全新的对象进行返回
LocalDateTime
是以上两者的结合,集成了所有内容;额外提供了方法,可以转换成以上的两者,也可以将以上两者转化成LocalDateTime
LocalDate ld = ldt.toLocalDate();
LocalTime lt = ldt.toLocalTime();
LocalDateTime ldt10 = LocalDateTime.of(ld,lt);
修改后的对象是一个全新的对象进行返回
ZoneId:时区
不同时区的时间不同
方法名 | 说明 |
---|---|
public static Set getAvailableZonelds() | 获取Java中支持的所有时区 |
public static Zoneld systemDefault() | 获取系统默认时区 |
public static Zoneld of(String zoneld) | 获取一个指定时区 |
ZonedDateTime:带时区的时间
方法名 | 说明 |
---|---|
public static ZonedDateTime now() | 获取当前时区的ZonedDateTime对象 |
public static ZonedDateTime now(Zoneld zone) | 获取指定时区的ZonedDateTime对象 |
getYear、getMonthValue、getDayOfMonth、getDayOfYeargetDayOfWeek、 getHour、getMinute、getSecond、getNano | 获取年月日、时分秒、纳秒等 |
public ZonedDateTime withXxx(时间) | 修改时间系列的方法 |
public ZonedDateTime minusXxx(时间) | 减少时间系列的方法 |
public ZonedDateTime plusXxx(时间) | 增加时间系列的方法 |
代替Date
Instant:时间戳/时间线
通过获取Instant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-0100:00:00开始走到此刻的总秒数+不够1秒的纳秒数
作用:可以用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。
传统的Date类,只能精确到毫秒,并且是可变对象;
新增的Instant类,可以 精确到纳秒 ,并且是不可变对象,推荐用Instant代替Date。
方法名 | 说明 |
---|---|
public static Instant now() | 获取当前时间的Instant对象 (标准时间) |
public long getEpochSecond() | 获取从1970-01-01T00:00:00开始记录的秒数。 |
public int getNano() | 从时间线开始,获取从第二个开始的纳秒数 |
plusMillis plusSeconds plusNanos | 判断系列的方法 |
minusMillis minusSeconds minusNanos | 减少时间系列的方法 |
equals、isBefore、isAfter | 增加时间系列的方法 |
代替SimpleDateFormat
DateTimeFormatter:用于时间的格式化和解析
方法名 | 说明 |
---|---|
public static DateTimeFormatter ofPattern(时间格式) | 获取格式化器对象 |
public String format(时间对象) | 格式化时间 |
LocalDateTime提供的格式化、解析时间的方法
方法名 | 说明 |
---|---|
public String format(DateTimeFormatter formatter) | 格式化时间 |
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) | 解析时间 |
//1、创建一个日期时间格式化器对象出来。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
//2、对时间进行格式化
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
String rs = formatter.format(now);
System.out.println(rs);
//3、格式化时间,其实还有一种方案。
String rs2=now.format(formatter);//反向格式化
System.out.println(rs2);
//4、解析时间:解析时间一般使用LocalDateTime提供的解析方法来解析。
String datestr="2029年12月12日12:12:11";
LocalDateTime ldt = LocalDateTime.parse(datestr,formatter);
System.out.println(ldt);
其他补充
Period:时间间隔(年,月,日)
可以用于计算两个LocalDate对象相差的年数、月数、天数。
方法名 | 说明 |
---|---|
public static Period between(LocalDate start, LocalDate end) | 传入2个日期对象,得到Period对象 |
public int getYears() | 计算隔几年,并返回 |
public int getMonths() | 计算隔几个月,年返回 |
public int getDays() | 计算隔多少天,并返回 |
Duration:时间间隔(时、分、秒,纳秒)
可以用于计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数;支持LocalTime、LocalDateTime、Instant等时间。
方法名 | 说明 |
---|---|
public static Duration between(开始时间对象1,截止时间对象2) | 传入2个时间对象,得到Duration对象 |
public long toDays() | 计算隔多少天,并返回 |
public long toHours() | 计算隔多少小时,并返回 |
public long toMinutes() | 计算隔多少分,并返回 |
public long toSeconds() | 计算隔多少秒,并返回 |
public long toMillis() | 计算隔多少毫秒,并返回 |
public long toNanos() | 计算隔多少纳秒,并返回 |
Arrays类
用于操作数组的工具类
常见方法:
方法名 | 说明 |
---|---|
public static String toString(类型[] arr) | 返回数组的内容 |
public static int[] copyOfRange(类型[] arr, 起始索引, 结束索引) | 拷贝数组(指定范围) |
public static copyOf(类型[] arr, int newLength) | 拷贝数组 |
public static setAll(double[] array, IntToDoubleFunction generator) | 把数组中的原数据改为新数据 |
public static void sort(类型[] arr) | 对数组进行排序(默认是升序排序) |
排序相关
对于对象想要实现排序:
-
让该对象的类实现Comparable(比较规则)接口,然后重写compareTo方法,自己来制定比较规则。若左边大于右边返回正整数,右大作返回负,相等返回0
@override public int compareTo(student o){return this.age - o.age; }
-
使用下面这个sort方法,创建Comparator比较器接口的匿名内部类对象,然后自己制定比较规则
public static<T> void sort(T[] arr,Comparator<?super T>c)
他的一个使用例子是
Arrays.sort(students,new Comparator<Student>(){@Overridepublic int compare(Student o1,Student o2){return Double.compare(o1.getHeight(),o1.getHeight());}});
JDK8新特性
Lambda表达式
Lambda表达式是JDK8开始新增的一种语法形式;
作用:用于简化匿名内部类的代码写法。
Lambda表达式只能简化函数式接口的匿名内部类
什么是函数式接?
- 有且仅有一个抽象方法的接口。
- 将来我们见到的大部分函数式接口,上面都可能会有一个@Functionallnterface的注解,有该注解
的接口就必定是函数式接口。
其形式如下:
(被重写方法的形参列表)->{被重写方法的方法体代码。
}
上面的sort传入的Comparator就是一个例子,可以先简化成
Arrays.sort(students,(Student o1,Student o2)->{return Double.compare(o1.getHeight(),02.getHeight());
});
Lambda表达式的省略规则
- 参数类型可以省略不写。
- 如果只有一个参数,参数类型可以省略,同时()也可以省略:s - > {System.Out.println(s.getNum()); return S.getSize();}
- 如果Lambda表达式中的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号!此时,如果这行代码是return语句,也必须去掉return不写。
此时上述代码精简成:
Arrays.sort(students,(o1,o2)-> Double.compare(o1.getHeight(),02.getHeight())
});
方法引用
方法引用的核心思想是:把已经有的方法拿过来用,当做函数式接口中抽象方法的方法体。
根据已有的方法的类型分为:静态方法、实例方法、
静态方法的引用
形式:
类名::静态方法
使用场景:
- 如果某个Lambda表达式里只是调用一个静态方法,并且前后参数的形式一致,就可以使用静态方法引用
其实就是将:
(o1,o2) -> CompareByData.compareByAge(o1,o2)
简化为:
CompareByData::compareByAge
其中的compareByAge是CompareByData的静态方法
实例方法的引用
形式:
实例名称::实例方法
使用场景:
- 如果某个Lambda表达式里只是调用一个实例方法,并且前后参数的形式一致,就可以使用实例方法引用
其实就是将:
CompareByData comareImpl = new CompareByData();(o1,o2) -> comareImpl.compareByAge(o1,o2)
简化为:
CompareByData comareImpl = new CompareByData();comareImpl::compareByAge
其中的compareByAge是CompareByData的实例方法
特定类型方法的引用
形式:
类型::方法
使用场景:
- 如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数是作为方法的主调,
后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型的方法引用
其实就是将:
(o1,o2) -> o1.compareToIgnoreCase(o2)
简化为:
String::compareToIgnoreCase
其中的o1是String类型的compareToIgnoreCase是他的一个方法
构造器引用(没啥应用)
形式:
类名::new
使用场景:
- 如果某个Lambda表达式里只是在创建对象,并且前后参数情况一致,就可以使用构造器引用。
其实就是将:
CreateCar cc = (name,price) -> new Car(name, price);
简化为:
CreateCar cc = Car::new;
JDK8 简化案例
背景
我们有一个 Student
类,包含姓名和身高两个字段。我们要按照身高升序对学生数组排序。并且已经写好了compareByHeight作为比较两个学生的方法
public class Student {private String name;private double height;public Student(String name, double height) {this.name = name;this.height = height;}public double getHeight() {return height;}public String getName() {return name;}// 已写好的比较方法public static int compareByHeight(Student s1, Student s2) {return Double.compare(s1.getHeight(), s2.getHeight());}
}
**研究对象:**原始排序代码(未简化)
Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student o1, Student o2) {return Student.compareByHeight(o1, o2);}
});
Lambda 简化
第一步:转为 Lambda 表达式(匿名内部类 → lambda)
Java 8 中 Comparator
是一个函数式接口,有一个抽象方法 int compare(T o1, T o2)
,因此可以使用 lambda 表达式简化。
Arrays.sort(students, (Student o1, Student o2) -> {return Student.compareByHeight(o1, o2);
});
第二步:类型推断(去掉参数类型)
Java 编译器可以从需要传入的第二个参数 Comparator<Student>
推断出参数类型。
Arrays.sort(students, (o1, o2) -> {return Student.compareByHeight(o1, o2);
});
第三步:表达式简化(去掉大括号和 return)
Lambda 表达式体是单条 return 语句时可省略 {}
和 return
。
Arrays.sort(students, (o1, o2) -> Student.compareByHeight(o1, o2));
至此Lambda的简化过程已经结束
方法引用简化
Lambda 表达式 (a, b) -> SomeClass.someMethod(a, b)
等价于 SomeClass::someMethod
,如果参数和方法签名一致。
Arrays.sort(students, Student::compareByHeight);
其他案例
如果一开始的实现方法是这样的
// 已写好的比较方法public int compareByHeight(Student other) {return Double.compare(this.getHeight(), other.getHeight());
}
将功能改成了:实例对象调用另一个对象来实现对应的比较
那么简化过程如下:
Arrays.sort(students, new Comparator<Student>() {@Overridepublic int compare(Student s1, Student s2) {return s1.compareByHeight(s2);}
});
Arrays.sort(students, (Student s1, Student s2) -> {return s1.compareByHeight(s2);
});
Arrays.sort(students, (s1, s2) -> {return s1.compareByHeight(s2);
});
Arrays.sort(students, (s1, s2) -> s1.compareByHeight(s2));
Arrays.sort(students, Student::compareByHeight);
编译器工作原理
Arrays.sort(students, Student::compareByHeight)↓ 编译器识别目标 Comparator<Student>↓ 推导方法引用签名为 (Student, Student) -> int↓ 转换为 lambda (s1, s2) -> s1.compareByHeight(s2)↓ 在运行时构造 lambda 实例(使用 LambdaMetafactory)↓ 传递 lambda 给 Arrays.sort↓ Arrays.sort 调用 compare(a[i], a[j])↓ 实际执行的是:a[i].compareByHeight(a[j])
Java 8 引入的方法引用(Method Reference)是 Lambda 表达式的简洁写法,编译器会根据上下文将其转换为对应形式的 Lambda 表达式。下面是编译器在处理方法引用时的判断与转换流程。
方法引用的类型判断依据
编译器判断转换方式主要依据以下两个因素:
- 方法引用左侧是 类名 还是 实例名
- 被引用的方法是 静态方法 还是 实例方法
方法引用出现↓
是否为实例对象(如 obj::method)→ 是 → 转换为 (x) -> obj.method(x)→ 否↓是否为静态方法(类名::staticMethod)→ 是 → 转换为 (x, y...) -> Class.staticMethod(x, y...)→ 否(为类名::实例方法)→ 转换为 (obj, arg...) -> obj.method(arg...)
Stream
JDK8最大的改变就是Stream和lambda表达式
Stream也叫Stream流,是JDK8开始新增的一套API (java.utiL.stream.*),可以用于操作集合或者数组的数据。
优势:Stream流大量的结合了Lambda的语法风格来编程,提供了一种更加强大,更加简单的方式操
作集合或者数组中的数据,代码更简洁,可读性更好
Stream流的使用步骤
1、确定我们的数据源
2、获取对应的Stream流
3、调用流水线的各种方法(过滤、排序、去重)对数据进行处理、计算
4、获取处理结果,遍历、统计、收集到一个新的集合中返回
获取流的方法
获取集合的Stream流
Collection提供的如下方法 | 说明 |
---|---|
default Stream<E> stream() | 获取当前集合对象的stream流 |
获取数组的Stream流
Arrays类提供的如下方法 | 说明 |
---|---|
public static <T> Stream stream <T>(T[] array) | 获取当前数组的stream流 |
Stream类提供的如下方法 | 说明 |
---|---|
public static <T> Stream <T> of(T… values) | 获取当前接收数据的stream流 |
注意,如果使用Stream的of方法,传入的应该是多个值,将多个值处理成一个流。
在前提定义了list如下:
List<Product> products;//下面这两个得到的流是不同的
Stream.of(products);// 得到的内容是只含一个元素(List<Product>)的流
products.stream();// 得到的内容是有多个元素(Product)的流
Stream流的中间方法
Stream提供的常用中间方法 | 说明 |
---|---|
Stream<T> filter(Predicate <? super T> predicate) | 用于对流中的数据进行过滤,可以使用lambda表达式 |
Stream<T> sorted() | 对元素进行升序排序 |
Stream<T> sorted(Comparator <? super T> comparator) | 按照指定规则排序 |
Stream<T> limit(long maxSize) | 获取前几个元素 |
Stream<T> skip(long n) | 跳过前几个元素 |
Stream<T> distinct() | 去除流中重复的元素,需要重写equals和HashCode方法 |
<R>Stream<T> map(Function<? super T,? extends R> mapper) | 对元素进行加工,并返回对应的新流 |
static <T> Stream<T> concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
终结方法
终结方法指的是调用完成后,不会返回新Stream了,没法继续使用流了
普通终结方法:
Stream提供的常用终结方法 | 说明 |
---|---|
void forEach(Consumer action) | 对此流运算后的元素执行遍历 |
long count() | 统计此流运算后的元素 |
Optional<T> max(Comparator <? super T> comparator) | 获取此流运算后的最大值元素 |
Optional<T> min(Comparator<? super T> comparator) | 获取此流运算后的最小值元素 |
收集Stream流:就是把Stream流操作后的结果转回到集合或者数组中去返回
Stream流:方便操作集合/数组的手段;
集合/数组:才是开发中的目的。
Stream提供的常用终结方法 | 说明 |
---|---|
R collect(Collector collector) | 把流处理后的结果收集到一个指定的集合中去 |
Object[] toArray() | 把流处理后的结果收集到一个数组中去 |
Collectors工具类提供了具体的收集方式 | 说明 |
---|---|
public static <T> Collector toList() | 把元素收集到List集合中 |
public static <T> Collector toSet() | 把元素收集到set集合中 |
public static Collector toMap(Function keyMapper , Function valueMapper) | 把元素收集到Map集合中 |
注意
- 流只能收集一次,如果调用了collect方法,就会关闭,再次调用会出错
正则表达式
String提供了一个匹配正则表达式的方法
名称 | 备注 |
---|---|
public boolean matches(String regex) | 判断字符串是否匹配正则表达式,匹配返回true,不匹配返回false。 |
除此之外,还有很多方法支持正则表达式,比如Pattern.compile()以及下文
方法名 | 说明 |
---|---|
public String replaceAll(String regex , String newStr) | 按照正则表达式匹配的内容进行替换 |
public String[] split(String regex) | 按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。 |
书写规则
字符类
规则 | 内容 |
---|---|
[abc] | 只能是a,b,或c |
[^abc] | 除了a,b,c之外的任何字符 |
[a-zA-Z] | a到zA到z,包括(范围) |
[a-d[m-p]] | a到d,或m到p |
[a-z&&[def]] | d,e,或f(交集) |
[a-z&&[^bc]] | a到z,除了b和c(等同于[ad-z]) |
[a-z&&[^m-p]] | a到z,除了m到p(等同于[a-lq-z]) |
预定义字符(只匹配单个字符)
规则 | 内容 |
---|---|
\d | 一个数字: [0-9] |
\D | 非数字: [^0-9] |
\s | 一个空白字符: |
\S | 非空白字符: [^\s] |
\w | [a-zA-Z_0-9] |
\W | [^\w] 一个非单词字符 |
数量词
规则 | 内容 |
---|---|
X? | X,0次或1次 |
X* | X,0或多次 |
X+ | X,1或多次 |
X{n} | X,刚好n次 |
X{n,} | X,至少n次 |
X{n,m} | X,n 到 m 次 |
其他
规则 | 内容 |
---|---|
(?i) | 忽略大小写 |
| | 或 |
() | 分组 |
可变参数
可变参数是JDK5开始支持的特性
就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型…参数名称;
特点:可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它。
好处:常常用来灵活的接收数据。
注意事项
- 可变参数在函数的内部实际上就是一个数组
- 一个形参列表中只能有一个可变参数
- 可变参数必须放在形参列表的最后面
异常
异常就是程序出现的问题
异常体系
处理异常
有两种方式:
- 抛出异常(throws):在方法上使用throws:关键字,可以将方法内部出现的异常抛出去给调用者处理。
- 捕获异常(try…catch):直接捕获程序出现的异常。
自定义异常
Java无法为这个世界上全部的问题都提供异常类来代表,如果企业自己的某种问题,想通过异常来表示,
以便用异常来管理该问题,那就需要自己来定义异常类了。
有两种自定义异常:自定义运行时异常、自定义编译时异常
自定义运行时异常
- 定义一个异常类继承RuntimeException.
- 重写构造器。
- 通过throw new异常类(Xx)来创建异常对象并抛出。
- 偏译阶段不报错,提醒不强烈,运行时才可能出现!!
自定义编译时异常
- 定义一个异常类继承Exception.
- 重写构造器。
- 通过throw new异常类(Xx)来创建异常对象并抛出。
- 编译阶段就报错,提醒更加强烈!
异常的作用
1、异常是用来查寻系统Bug的关键参考信息
2、异常以作为方法内部的一种特殊返回值,以便通和上层调用者底层的执行情况!
- **保证程序的稳定性和健壮性。**异常是程序运行过程中的常态,可以及时发现并处理这些问题,防止程序崩溃或出现其他严重的后果。
- 提高代码的可读性和可维护性。通过异常的处理机制,可以将错误信息以更容易理解和维护的方式呈现给开发者,有利于代码的修改和优化。
- **提高程序的可靠性。**异常可以被用来判断程序是否正常运行,以及记录程序中的错误信息,从而提高程序的可靠性和稳定性。
- **促进代码的重用和复用。**通过捕获和处理异常,可以将程序中的错误信息和处理逻辑封装成一个独立的模块,方便其他代码重用和复用。
异常的常见处理方式
1、捕获异常,记录异常并响应合话的信息给用户
2、捕获异常,尝式重新修复
集合进阶
Java的集合分为两种:单例集合Collection、双列集合Map
Collection
Collection集合特点
- List系列集合:添加的元素是有序、可重复、有索引
- ArrayList、LinekdList:有序、可重复、有索引
- Set系列集合:添加的元素是无序、不重复、无索引
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
- TreeSet:按照大小默认升序排序、不重复、无索引
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的遍历方式
Collection是不支持直接使用for循环进行遍历的
迭代器
迭代器是用来遍历集合的专用方式(数组没有迭代器),在Java中迭代器的代表是Iterator
获取迭代器对象的方法:
方法名称 | 说明 |
---|---|
Iterator iterator() | 返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素 |
迭代器的常用方法:
方法名称 | 说明 |
---|---|
boolean hasNext() | 询问当前位置是否有元素存在,存在返回true ,不存在返回false |
E next() | 获取当前位置的元素,并同时将迭代器对象指向下一个元素处 |
使用示例:
Iterator<String>it lists.iterator();
while(it.hasNext()){String ele = it.next();System.out.println(ele);
)
如果越界了会出现:没有此元素异常
增强for循环
其实增强for循环的本质就是使用迭代器的简化写法 他也可以用于数组
格式:
for(元素的数据类型 变量名:数组或者集合){}
lambda表达式
Collection提供了一个forEach方法
default void forEach(Consumer<?super T>action)
他的实际默认实现代码如下:
default void forEach(Consumer<?super T>action){Objects.requireNonNull(action);for (T t:this){action.accept(t);}
}
可以采用匿名内部类完成实现对应的功能
c.forEach(new Consumer<String>(){@Overridepublic void accept(String s){System.out.println(s);}
});
如果采用lambda表达式简化上述结果的话就是:
c.forEach(s->System.out.println(s));
其实还可以接着使用方法引用:
c.forEach(System.out::println);
List
List的特有方法
List集合因为支持索引,所以多了很多与索引相关的方法,当然,Collection的功能List也都继承了。
个性方法
方法名称 | 说明 |
---|---|
void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
遍历方法
支持Collection的三种遍历方法
因为是有索引的,所以也支持直接使用for循环遍历
ArrayList
特点
- 基于数组实现
- 根据索引查询速度较快
- 删除效率低(线性前移)
- 添加效率低(线性后移、数组扩容)
底层原理
- 利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组
- 添加第一个元素时,底层会创建一个新的长度为10的数组
- 存满时,会扩容1.5倍
- 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
适合场景
根据索引查找数据 || 数据量不大
不适合场景
数据量大,且频繁增删
LinkedList
特点
- 基于双链表实现
- 查询慢,从头找
- 增删相对快,找到后增删单个节点
- 对首尾元素的增删改查是非常快的
特有方法
方法名称 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
应用场景
- 设计队列
- 设计栈(LinkedList给出了默认的push、pop方法,就是实现栈的)
Set
特点
- 无序:添加数据的顺序和获取出的数据顺序不一致;
- 不重复;
- 无索引;
实现类的特点
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
- TreeSet:排序(默认升序)、不重复、无索引
特有功能
Set要用到的常用方法,基本上就是Collection提供的
自己几乎没有额外新增一些常用功能
HashSet
底层原理
-
基于哈希表实现
-
哈希表是一种增删改查数据,性能都较好的数据结构
-
JDK8之前,哈希表的实现方式是=数组+链表
- 第一次添加数据时,创建一个默认长度16的数组,默认加载因子为0.75
- 使用元素的哈希值对数组长度求余计算出应该存的位置
- 判断当前位置是否为null,如果是null直接存入
- 如果不为null,表示有元素,则调用equals方法比较
- 相等,则不存;不相等,则存入数组(哈希冲突)
- JDK8之前,新元素存入数组,占老元素位置,老元素挂下面
- JDK8开始之后,新元素直接挂在老元素下面
-
如果占位的数量达到了:总长度*加载因子 的长度,那么就会进行扩容,将当前容量翻倍,并重新插入原本的值
-
JDK8开始,哈希表的实现方式是=数组+链表+红黑树
-
JDK8开始,当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树
特点
哈希表是一种增删改查数据性能都较好的结构。
但是如果数组快占满会导致链表过长、查询性能降低,此时会进行扩容
哈希表去重
对于内容一样的不同对象,HashSet是不会去重的,因为她去重的原理是对哈希冲突的对象进行equals方法比较
因此想要实现对象之间的去重,就要重写hashCode()方法以及equals()方法
重写hashCode一般是采用
Objects.hash(name,age,height)
LinkedHashSet
他最大的特点就是有序的,可以记录存入的顺序
底层原理
- 依然是基于哈希表(数组、链表、红黑树)实现的。
- 但是,它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。
- 每个新进入的元素都会记录前后位置关系,因此是
TreeSet
他最大的特点在于是可排序的,默认是升序的
底层原理
底层是基于红黑树实现的排序
自定义排序
- 对于自定义类型如Student对象,TreeSet默认是无法直接排序的
- TreeSet集合存储自定义类型的对象时,必须指定排序规则,支持如下两种方式来指定比较规则
- 让自定义的类(如学生类)实现Comparable接口,重写里面的compareTo方法来指定比较规则
- 通过调用TreeSet:集合有参数构造器,可以设置Comparator对象(比较器对象,用于指定比较规则
- 注意相等的情况是会被删除的
总结
-
如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据?
- 用ArrayList集合(有序、可重复、有索引),底层基于数组的。(常用)
-
如果希望记住元素的添加顺序,且增删首尾数据的情况较多?
- 用LinkedList:集合(有序、可重复、有索引),底层基于双链表实现的。
-
如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快?
- 用HashSet集合(无序,不重复,无索引),底层基于哈希表实现的。(常用)
-
如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快?
- 用LinkedHashSet:集合(有序,不重复,无索引),底层基于哈希表和双链表。
-
如果要对元素进行排序,也没有重复元素需要存储?且希望增删改查都快?
- 用TreeSet:集合,基于红黑树实现。
集合的并发修改异常
内容
集合的并发修改异常问题指的是在使用迭代器遍历集合的过程中,如果在迭代的过程中修改了集合的结构(如增加、删除元素),就会抛出ConcurrentModificationException异常。
底层原理
这是因为在遍历集合时,迭代器会维护一个期望的修改次数,当发现集合的结构被修改时,就会抛出并发修改异常,以避免遍历过程中数据的不一致性。
回顾使用ArrayList时遍历并删除可能出现的错误:
//使用for循环遍历集合并删除集合中带李字的名字
//[王麻子,小李子,李爱花,张全蛋,晓李,李玉刚]for (int i=0;i<list.size();i++){String name list.get(i);if(name.contains("李")){list.remove(name);}
}
//[王麻子,李爱花,张全蛋,李玉刚]
sout(list)
这是因为,删除一个元素后后续的元素会依次补上空缺的位置,因此,每删除一个元素都会漏过一个元素没有检查。
相同的,使用其他集合也会出现对应的集合的并发修改异常:
Iterator<String>it = list.iterator();
while (it.hasNext()){String name = it.next();if(name.contains("李")){//List.remove(name)//并发修改异常的错误。it.remove();//删除迭代器当前遍历到的数据,每删除一个数据后,相当于也在底层做了i--}
}
System.out.println(list);
应该使用迭代器自带的remove方法,不然会出错。
对于使用增强for循环和使用forEach方法,如果删除数据的话也会有对应的集合的并发修改异常,并且无法修复错误。
工具类
有用于操作集合的工具类Collections
常用方法
方法名称 | 说明 |
---|---|
public static boolean addAll(Collection c, T…elements) | 给集合批量添加元素 |
public static void shuffle(List list) | 打乱List集合中的元素顺序 |
public static void sort(List list) | 对List集合中的元素进行升序排序 |
public static void sort(List list, Comparator c) | 对List集合中元素,按照比较器对象指定的规则进行排序 |
Map
- Map集合称为双列集合,格式:{key1=value1 key2=value2,key3=value3,…}每次需要存一对数据做为一个元素
- Map集合的每个元素“key=value”称为一个键值对/键值对对象/一个Entry对象,Map集合也被叫做"键值对集合”
- Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值
Map集合体系
Map集合体系的特点:
- 注意:Map系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的
- HashMap(由键决定特点):无序、不重复、无索引;(用的最多)
- LinkedHashMap(由键决定特点):由键决定的特点:有序、不重复、无索引。
- TreeMap(由键决定特点):按照大小默认升序排序、不重复、无索引。
常用方法
方法名称 | 说明 |
---|---|
public V put(K key,V value) | 添加元素 |
public int size() | 获取集合的大小 |
public void clear() | 清空集合 |
public boolean isEmpty() | 判断集合是否为空,为空返回true,反之 |
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集合的全部值 |
Map的遍历方式
键找值
需要用到以下方法:
方法名称 | 说明 |
---|---|
public Set keySet() | 获取所有键的集合 |
public V get(Object key) | 根据键获取其对应的值 |
Set<String>keys map.keyset();
for (String key:keys){double value map.get(key);System.out.println(key "=====>"
value);
}
键值对
需要用到以下方法
Map提供的方法 | 说明 |
---|---|
Set<Map.Entry> entrySet() | 获取所有 “键值对" 的集合 |
Map.Entry提供的方法 | 说明 |
---|---|
K getKey() | 获取键 |
V getValue() | 获取值 |
//1、调用Map集合提供entrySet.方法,把Map集合转换成键值对类型的Set集合
Set<Map.Entry<String,Double>>entries map.entrySet();
for (Map.Entry<String,Double>entry entries){String key entry.getKey();double value entry.getValue();System.out.println(key "---->"value);
}
forEach+Lambda
最推荐使用,但是是从jdk8开始才有的
用到的方法:
方法名称 | 说明 |
---|---|
default void forEach(BiConsumer<? suoer K,? super V> action) | 结合lambda遍历Map集合 |
map.forEach((K,v)-> System.out.println(k+"--->"+v) );
其实上述内容是通过下述内容简化过来的
map.forEach(new BiConsumer<string,Double>(){@overridepublic void accept(String k,Double v){System.out.println(k "---->"v);}
forEach的底层实现是通过Map.Entry实现的增强for循环
Map的具体实现类
HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。
实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。
所以这部分内容查看Set部分的相关内容
多线程
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
Java是通过java.lang.Thread类的对象来代表线程的。
实现多线程的方法
继承Thread类
1、子类继承Thread线程类
2、重写Thread类的run方法
3、在主线程创建线程对象,并调用start()方法
public class MyThread extends Thread{@overridepublic void run(){for(int i=1;i<6;++i)System.Out.println("子线程输出"+i);}
}
public class Test{public static void main(String[] args){Thread t = new MyThread();t.start();for(int i=1;i<6;++i)System.Out.println("主线程输出"+i);}
}
注意
启动线程必须是调用start方法,不是调用run方法。
不要把主线程任务放在启动子线程之前。
实现Runnable接口
1、定义任务类实现Runnable接口
2、重写Runnable接口的run方法
3、在主线程创建对应的任务对象
4、创建线程对象,该对象传入上述实现Runnable接口的任务对象,并调用start()方法
public class MyRunnable implements Runnable{@overridepublic void run(){for(int i=1;i<6;++i)System.Out.println("子线程输出"+i);}
}
public class Test{public static void main(String[] args){Runnable target = new MyRunnable();Thread t = new MyThread(target);t.start();for(int i=1;i<6;++i)System.Out.println("主线程输出"+i);}
}
或是使用匿名内部类实现辅助创建
1、可以直接在主线程创建Runnable的匿名内部类对象。
2、再交给Thread线程对象。
3、再调用线程对象的start()启动线程。
public class Test{public static void main(String[] args){Runnable target = new Runnable(){@overridepublic void run(){for(int i=1;i<6;++i)System.Out.println("子线程输出"+i);}};Thread t = new MyThread(target);t.start();for(int i=1;i<6;++i)System.Out.println("主线程输出"+i);}
}
或者一步简化到位
public class Test{public static void main(String[] args){Thread t = new MyThread(()->{for(int i=1;i<6;++i)System.Out.println("子线程输出"+i);});t.start();for(int i=1;i<6;++i)System.Out.println("主线程输出"+i);}
}
public class Test{public static void main(String[] args){Runnable target = new MyRunnable();Thread t = new MyThread(target);t.start();for(int i=1;i<6;++i)System.Out.println("主线程输出"+i);}
}
实现Callable接口,并封装成FutureTask类
1、创建任务对象
-
定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
-
把Callable类型的对象封装成FutureTask(线程任务对象)。
2、把线程任务对象交给Thread对象。
3、调用Thread对象的start方法启动线程。
4、线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
public class MyCallable implements Callable<String>{private int n;public MyCallable(int n){this.n = n;}@overridepublic String call() throws Exception{int sum =0;for(int i=1;i<=n;++i)sum+=i;return ""+sum;}
}
public class Test{public static void main(String[] args){Callable<String> call = new Mycallable(100);FutureTask<String> f1 = new FutureTask<>(call);new Thread(f1).start();String rs = f1.get();System.out.println(rs);
}
使用FutureTask 的get方法的时候会等待对应的线程结束之后得到对应的值。
线程的常用方法
Thread提供的常用方法 | 说明 |
---|---|
public void run() | 线程的任务方法 |
public void start() | 启动线程 |
public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
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对象成为线程对象,并指定线程名称 |
线程安全
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
线程同步
用于解决线程安全问题
让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的常见方案:
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
加锁有三种常见方案:1、同步代码块 2、同步方法 3、Lock锁
同步代码块
把访问共享资源的核心代码给上锁,以此保证线程安全。
synchronized(同步锁){访问共享资源的核心代码
}
每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
全。
synchronized("getMoney"){if(this.money>=money){this.monet -= money;}
}
以上是一种实现方法,但是直接使用相同的字符串会导致所有需要执行这段代码的对象都会互斥,哪怕他们需要访问的不是同一个资源
建议使用共享资源作为锁,比如实例方法使用this,而静态方法使用 类名.class
同步方法
把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型方法名称(形参列表){操作共享资源的代码
}
每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵话、更方便、更强大。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
一种常见的方式是使用单例设计模式,在我们需要加锁的对象当中设计一个锁对象的单例。之后在需要访问资源的时候使用Lock对象进行lock和unlock,其中unlock最好放在try-catch-finally的finally当中
public class Account{private String cardId;//卡号private double money;//余额。//创建了一个锁对象private final Lock lk = new ReentrantLock();
}
线程通信
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的前提是线程安全:要在内层保证资源互斥
实现线程通信需要用到:
Object类的等待和唤醒方法
方法名称 | 说明 |
---|---|
void wait() | 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待的单个线程 |
void notifyAll() | 唤醒正在等待的所有线程 |
但是在实际使用的时候应该使用当前的同步锁对象使用上述的方法
详细的例子见实践部分
Condition
Java中的Condition
接口提供了比传统synchronized
配合wait()
和notify()
更灵活的线程等待/通知机制。结合Lock
使用,Condition
允许更细粒度的线程控制。以下是其核心机制与使用要点的总结:
- await():释放当前锁,使线程进入等待状态,直到以下情况发生:
- 被signal()或signalAll()唤醒
- 线程被中断(抛出InterruptedException)
- 支持超时或指定截止时间的变体方法(如awaitNanos()、awaitUntil())
- signal():唤醒一个等待在此Condition上的线程(随机选择)
- signalAll():唤醒所有等待在此Condition上的线程
线程生命周期
Java定义了6种状态
-
NEW:新建
-
RUNNABLE:可运行
-
BLOCKED:阻塞
-
WAITING:无限等待
-
TIMED WAITING:计时等待
-
TERMINATED:被终止
并发和并行
单核上的处理过程是并发的,需要线程调度,切换环境
多核之间,每个逻辑单元上是并行的。是同时处理的。
线程池
线程池就是一个可以复用线程的技术。
用于减小创建和销毁所带来的资源消耗
主要有两种内容:
1、工作线程,核心线程;
2、任务队列,当中的任务需要实现任务接口(Runnable、Callable)
得到线程池
JDK5开始提供了代表线程池的接口ExecutorService,其中用的比较多的实现类是ThreadPoolExecutor
所以有两种方式得到线程池对象:
- 使用ExecutorServicel的实现类ThreadPoolExecutorl自创建一个线程池对象
- 使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor构造器
- 参数一:corePoolSize:指定线程池的核心线程的数量
- 参数二:maximumPoolSize:指定线程池的最大线程数量
- 参数三:keepAliveTime:指定临时线程的存活时间
- 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
- 参数五:workQueue:指定线程池的任务队列
- 参数六:threadFactory:指定线程池的线程工厂
- 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
一个实现的构造如下:
ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defauleThreadFactort(), new ThreadPoolExecutor.AbortPolicy())
常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行 Runnable 任务 |
Future submit(Callable task) | 执行Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
List shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
常用拒绝策略
- AbortPolicy
丢弃并抛出RejectedExecutionException异常
- DiscardPolicy
直接丢弃
- DiscardOldestPolicy
直接丢弃最前面的任务,尝试执行新任务
- CallerRunsPolicy
由调用线程池的线程处理任务,可以是主线程,也可能是其他创建线程池的线程。
注意事项
- 临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。有多重拒绝策略,用到的时候再看。
以上述构造器为例,3个任务会直接执行,3+4个任务会进入任务队列,3+4+2个任务会创建临时线程,3+4+2+n会对新任务进行拒绝
- 核心线程数量配置
对于计算密集型,CPU核数+1
对于IO密集型,CPU核数*2
线程池使用
执行Runnable任务只需要使用Pool.execute(准备执行的任务)就好了
执行Callable任务则需要使用一个Future<> f来接pool.submit(准备执行的任务)返回的对象。想要得到线程返回的对象,只需要使用f.get()方法
使用工具类Executors
是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。但是大型并发项目使用Executors可能会出现系统问题
方法名称 | 说明 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异 常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常 而结束,那么线程池会补充一个新线程。 |
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕 且空闲了60s则会被回收掉。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务, 或者定期执行任务。 |
这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
File、IO流
File类只能对文件本身进行操作,不能读写文件里面存储的数据。
IO流是用于读写数据的,可以读写文件、或网络中的数据
File类
绝对路径、相对路径
绝对路径:从盘符开始
相对路径:不带盘符,默认直接到当前工程下的目录寻找文件
File创建对象
构造器 | 说明 |
---|---|
public File(String pathname) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 根据父路径和子路径名字创建文件对象 |
public File(File parent, String child) | 根据父路径对应文件对象和子路径名字创建文件对象 |
File提供的功能
方法名称 | 说明 |
---|---|
public boolean exists() | 判断当前文件对象,对应的文件路径是否存在,存在返回true |
public boolean isFile() | 判断当前文件对象指代的是否是文件,是文件返回true,反之。 |
public boolean isDirectory() | 判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之。 |
public String getName() | 获取文件的名称 (包含后缀) |
public long length() | 获取文件的大小,返回字节个数 |
public long lastModified() | 获取文件的最后修改时间。 |
public String getPath() | 获取创建文件对象时,使用的路径 |
public String getAbsolutePath() | 获取绝对路径 |
创建文件
方法名称 | 说明 |
---|---|
public boolean createNewFile() | 创建一个新的空的文件 |
public boolean mkdir() | 只能创建一级文件夹 |
public boolean mkdirs() | 可以创建多级文件夹 |
删除文件
方法名称 | 说明 |
---|---|
public boolean delete() | 删除文件、 空文件夹 |
遍历文件夹
“一级文件”是指显示在文件夹当中的内容,包括子文件夹、文件;但是不包含子文件夹中的内容
-
遍历“一级文件”名称
//1、public String[]Lis):获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 File f1=new File("D:\\course\\待研发内容"); String[] names f1.list(); for (String name names){System.out.println(name); }
-
遍历“一级文件”对象
//2、public File[]listFiles():(重点)获取当前目录下所有的"一级文件对象" File[]files f1.listFiles(); for (File file files){System.out.println(file.getAbsolutePath()); }
注意
- delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站
- 使用listFiles方法时的注意事项:
- 当主调是文件,或者路径不存在时,返回null
- 当主调是空文件夹时,返回一个长度为0的数组
- 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回
- 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件
- 当主调是一个文件夹,但是没有权限访问该文件夹时,返回null
前置知识
递归
方法递归:形式上说,方法调用自身的方式称为方法递归
递归的形式主要有:直接递归和间接递归。间接递归是方法调用其他方法,其他方法再调用回方法本身
字符集
ASCII字符集
一个字节存一个字符,一共128个
GBK
国标汉字内码扩展规范
一个中文编码成两个字节,第一位是1
兼容ASCII,第一位是0
Unicode字符集
国际组织制定,容纳所有文字、符号的字符集
UTF-32
四字节表示一个字符,占存储空间,通信效率很低
UTF-8
是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节
- 英文字符、数字等只占1个字节(兼容标准ASCI川编码)
- 汉字字符占用3个字节。
四个长度区的编码规则如下:
UTF-8编码方式(二进制) |
---|
0xxxxxxx (ASCII码) |
110xxxxx 10xxxxxx |
1110xxxx 10xxxxxx 10xxxxxx |
11110xxx 10xxxxxx 10xxxxxx 10xxxXxx |
注意
编码和解码应该使用相同的字符集,否则会出现乱码
英文、数字一般不会乱码,因为很多字符集兼容了ASCII编码
Java进行字符编码、解码
编码:
String提供了如下方法 | 说明 |
---|---|
在byte[] getBytes() | 使用平台的默认字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
byte[] getBytes(String charsetName) | 使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
解码:
String提供了如下方法 | 说明 |
---|---|
String(byte[] bytes) | 通过使用平台的默认字符集解码指定的字节数组来构造新的 String |
String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来构造新的 String |
//1、编码
String data="a我b";
byte[] bytes=data.getBytes();//默认是按照平台字符集(UTF-8)进行编码的
//按照指定字符集进行编码。
byte[] bytes1 = data.getBytes("GBK");
System.out.println(Arrays.tostring(bytes1));//2、解码
String s1=new String(bytes);//按照平台默认编码(UTF-8)解码
System.out.println(s1);String s2=new String(bytes,"GBK");//指定字符集进行解码
System.out.println(s2);
IO流
I指Input,称为输入流:负责把数据读到内存中去
O指Output,称为输出流:负责写数据出去
IO流的类型
字节输入流、字节输出流
字符输入流、字符输出流
IO流的体系
- 如果操作的是纯文本文件,优先使用字符流
- 如果操作的是图片、视频、音频等二进制文件,优先使用字节流
- 如果不确定文件类型,使用字节流。字节流是万能流
字节流
FilelnputStream
作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。
**适合场景:**实际上字节流更适合做数据的转移,比如文件复制,读写文本内容更适合用字符流
构造器 | 说明 |
---|---|
public FileInputStream(File file) | 创建字节输入流管道与源文件接通 |
public FileInputStream(String pathname) | 创建字节输入流管道与源文件接通 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字节返回,如果发现没有数据可读会返回-1. |
public int read(byte[] buffer) | 每次用一个字节数组去读取数据,返回字节数组读取了多少个字节, 如果发现没有数据可读会返回-1. |
一次读取多个,但是依然有中文乱码问题
//3、使用循环改造。
byte[] buffer new byte[3];
int len;//记住每次读取了多少个字节。
while((len = is.read(buffer))!=-1){//注意:读取多少,倒出多少,防止最后的过量数据String rs = new String(buffer,0,len);System.out.print(rs);
}
is.close();//关闭流
一次读取完所有字节:
方案一:字节数组大小和文件大小一致(只适用于读文件比较小的情况,对于文件大小超过可用内存大小的情况会出问题,实际上超过int大小就会出问题)
//2、准备一个字节数组,大小与文件的大小正好一样大。
File f = new File("file-io-app\\src\\itheima03.txt");
Long size f.Length();
byte[] buffer = new byte[(int)size];
方案二:Java官方为InputStream提供了如下方法,可以直接把文件的全部字节读取到一个字节数组中返回,原理和上面差不多,不同的是会抛出一个异常显示文件太大了
方法名称 | 说明 |
---|---|
public byte[] readAllBytes() throws IOException | 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
注意
- 使用FilelnputStream每次读取一个字节,读取性能较差,并且读取汉字输出会乱码。
- 使用FilelnputStream每次读取多个字节,读取性能得到了提升,但读取汉字输出还是会乱码。
- 用byte存储read后的字节是从前往后覆盖的,如果原先就有内容且内容比新读入的常,旧内容是依然存在的,最好结合返回的读取长度进行后续操作
FileOutputStream
作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。
构造的时候会自动创建一个文件对象
构造器 | 说明 |
---|---|
public FileOutputStream (File file) | 创建字节输出流管道与源文件对象接通,覆盖式,调用构造器会清除旧数据 |
public FileOutputStream(String filepath) | 创建字节输出流管道与源文件路径接通,覆盖式,调用构造器会清除旧数据 |
public FileOutputStream(File file, boolean append) | 创建字节输出流管道与源文件对象接通,可追加数据 |
public FileoutputStream(String filepath, boolean append) | 创建字节输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
public void write(int a) | 写一个字节出去 |
public void write(byte[] buffer) | 写一个字节数组出去 |
public void write(byte[] buffer ,int pos ,int len) | 写一个字节数组的一部分出去。 |
public void close() throws IOException | 关闭流。 |
示例
byte[] bytes="我爱你中国abc".getBytes();//注意字节数组的特别形式
os.write(bytes);
os.write(bytes,0,15);//UTF-8中文占3字节,此处输出abc
//需求:复制照片。
//1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
//2、创建一个字节输出流管道与目标文件接通,直接用覆盖式的就行,因为是在调用构造器的时候清除旧数据
OutputStream os = new FileOutputStream("C:/data/meinv.png");
//1024+1024+6
//3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024];//1KB.
//4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len;//记住每次读取了多少个字节。
while ((len = is.read(buffer))!=-1){os.write(buffer,0,len);
}
os.close();
is.close();
System.out.println("复制完成!!");
适合的应用场景
任何文件的底层都是字节,字节流做复制,是一字不漏的转移完;全部字节,只要复制后的文件格式一致就没问题!
字符流
FileReader输入
以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。
构造器 | 说明 |
---|---|
public FileReader(Filefile) | 创建字符输入流管道与源文件接通 |
public FileReader(String pathname) | 创建字符输入流管道与源文件接通 |
方法名称 | 说明 |
---|---|
public int read() | 每次读取一个字符返回,如果发现没有数据可读会返回-1. |
public int read(char[] buffer) | 每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,如果发现没有数据可读会返回-1. |
FileWriter(文件字符输出流)
以内存为基准,把内存中的数据以字符的形式写出到文件中去。
构造器 | 说明 |
---|---|
public FileWriter(File file) | 创建字节输出流管道与源文件对象接通 |
public FileWriter(String filepath) | 创建字节输出流管道与源文件路径接通 |
public FileWriter(File file, boolean append) | 创建字节输出流管道与源文件对象接通,可追加数据 |
public FileWriter(String filepath, boolean append) | 创建字节输出流管道与源文件路径接通,可追加数据 |
方法名称 | 说明 |
---|---|
void write(int c) | 写一个字符 |
void write(String str) | 写一个字符串 |
void write(String str, int off, int len) | 写一个字符串的一部分 |
void write(char[] cbuf) | 写入一个字符数组 |
void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
void close() | 关闭流 |
void flush() | 刷新流 |
注意
- 换行使用"\r\n"因为需要将’\'进行转义
- 字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效,因为字符输出流是先将数据写到缓冲区当中,只有在刷新(自动或手动)或者关闭的时候会将缓冲区的数据通过系统调用写入磁盘
缓冲流
之前的File***的IO流被称为原始流 / 低级流。与之对应的是包装流 / 处理流,用于对原始流进行包装,提高原始流读写数据的性能。
字节缓冲流
原理:字节缓冲输入流自带了8KB缓冲池;字节缓冲输出流也自带了8KB缓冲池
构造器 | 说明 |
---|---|
public BufferedInputStream(InputStream is) | 把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读数据的性能 |
public BufferedOutputStream(OutputStream os) | 把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 |
字符缓冲流
输入
作用:自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能
构造器 | 说明 |
---|---|
public BufferedReader(Reader r) | 把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读字符数据的性能 |
新增功能:
方法 | 说明 |
---|---|
public String readLine() | 读取一行数据返回,如果没有数据可读了,会返回null |
输出
原始的字符输出流自带的缓冲池比较小,使用字符缓冲输出流默认的缓冲池是8K的,可以提高输出效率,注意要在关闭流之前调用flush确保写入
构造器 | 说明 |
---|---|
public BufferedWriter(Writer r) | 把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能 |
新增方法:
方法 | 说明 |
---|---|
public void newLine() | 换行 |
public void flush() | 刷新写入 |
其他流
转换流
如果代码编码和被读取的文本文件的编码是不一致的,使用字符流读取文本文件时就会出现乱码!
转换流是字符流的一个实现类
输入流
他的解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。
构造器 | 说明 |
---|---|
public InputStreamReader(InputStream is) | 把原始的字节输入流,按照代码默认编码转成字符输入流(与直接用FileReader的效果一样) |
public InputStreamReader(InputStream is , String charset) | 把原始的字节输入流,按照指定字符集编码转成字符输入流(重点) |
try(//1、得到文件的原始字节流(GBK的字节流形式)InputStream is = new FileInputStream("io-app2/src/itheima06.txt");//2、把原始的字节输入流按照指定的字符集编码转换成字符输入流Reader isr = new InputStreamReader(is,"GBK");//3、把字符输入流包装成缓冲字符输入流BufferedReader br = new BufferedReader(isr);
){String line;while ((line = br.readLine())!=null){System.out.println(line);}
}catch (Exception e){e.printstackTrace();
}
输出流
他的解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了。
构造器 | 说明 |
---|---|
public OutputStreamWriter(OutputStream os) | 可以把原始的字节输出流,按照代码默认编码转换成字符输出流。 |
public OutputStreamWriter(OutputStream os,String charset) | 可以把原始的字节输出流,按照指定编码转换成字符输出流(重点) |
//指定写出去的字符编码。
try(//1、创建一个文件字节输出流Outputstream os = new Fileoutputstream("io-app2/src/itheima07out.txt");//2、把原始的字节输出流,按照指定的字符集编码转换成字符输出转换流。Writer osw = new OutputStreamWriter(os,"GBK");//3、把字符输出流包装成缓冲字符输出流BufferedWriter bw = new BufferedWriter(osw);
){bw.write("我是中国人abc");bw.write("我爱你中国123");
}catch (Exception e){e.printstackTrace();
}
上述代码将原始流先包装成转换流用于输出GBK编码,之后用缓冲流包装用于提高输出效率。输出的时候会先进入缓冲流管道,之后再进入转换流转换编码,最后通过文件字节流输出。
打印流
PrintStream是字节输出流的打印流实现
PrintWriter是字符输出流的打印流实现
打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去 。
PrintStream
构造器:
构造器 | 说明 |
---|---|
public PrintStream(OutputStream/File/String) | 打印流直接通向字节输出流/文件/文件路径 |
public PrintStream(String fileName, Charset charset) | 可以指定写出去的字符编码 |
public PrintStream(OutputStream out, boolean autoFlush) | 可以指定实现自动刷新 |
public PrintStream(OutputStream out, boolean autoFlush, String encoding) | 可以指定实现自动刷新,并可指定字符的编码 |
方法:
方法 | 说明 |
---|---|
public void printIn(Xxx xx) | 打印任意类型的数据出去 |
public void write(int/byte[]/byte[]—部分) | 可以支持写字节数据出去 |
PrintWriter
构造器:
构造器 | 说明 |
---|---|
public PrintWriter(OutputStream/Writer/File/String) | 打印流直接通向字节输出流/文件/文件路径 |
public PrintWriter(String fileName, Charset charset) | 可以指定写出去的字符编码 |
public PrintWriter(OutputStream out/Writer, boolean autoFlush) | 可以指定实现自动刷新 |
public Printwriter(OutputStream out, boolean autoFlush, String encoding) 可以指定实现自动刷新,并可指定字符的编码 |
方法:
方法 | 说明 |
---|---|
public void printIn(Xxx xx) | 打印任意类型的数据出去 |
public void write(int/byte[]/byte[]—部分) | 可以支持写字符数据出去 |
注意
-
PrintWriter和PrintStream只有write方法效果是不同的其余都是差不多的实现
-
打印流想要实现追加必须传入低级流File****的对应对象才行
-
打印流自带缓冲区
应用
输出重定向:把输出语句的打印位置修改到某个文件当中去。
System.out得到的其实是一个打印流对象
System自带一个setOut(PrintStream ps)方法,可以用于重定向
PrintStream ps = new PrintStream("文件地址");
System.setOut(ps);
System.Out.println("Something");
此时的输出就会输出到文件地址当中。
数据流
数据输出流允许把数据和其类型一并写出去,其输出的是特殊形式,不方便阅读,但是方便下次一并读入
有两种流,DataInputStream、DataOutputStream都是字节流的实现
输出构造器:
构造器 | 说明 |
---|---|
public Data0utputStream(OutputStream out) | 创建新数据输出流包装基础的字节输出流 |
方法:
方法 | 说明 |
---|---|
public final void writeByte(int v) throws IOException # | 8正在观看视频 将byte类型的数据写入基础的字节输出流 |
public final void writeInt(int v) throws IOException | 将int类型的数据写入基础的字节输出流 |
public final void writeDouble(Double v) throws IOException | 将double类型的数据写入基础的字节输出流 |
public final void writeUTF(String str) throws IOException | 将字符串数据以UTF-8编码成字节写入基础的字节输出流 |
public void write(int/byte[]/byte[]—部分) | 支持写字节数据出去 |
输入构造器
构造器 | 说明 |
---|---|
public DataInputStream(InputStream is) | 创建新数据输入流包装基础的字节输入流 |
方法 | 说明 |
---|---|
Public final byte readByte() throws IOException | 读取字节数据返回 |
public final int readInt() throws IOException | 读取int类型的数据返回 |
public final double readDouble() throws IOException | 读取double类型的数据返回 |
public final String readUTF() throws IOException | 读取字符串数(UTF-8)据返回 |
public int readInt()/read(byte[]) | 支持读字节数据进来 |
序列化流
对象序列化:把Java对象写入到文件中去
对象反序列化:把文件里的Java对象读出来
ObjectInputStream
构造器 | 说明 |
---|---|
public 0bjectInputStream(InputStream is) | 创建对象字节输入流,包装基础的字节输入流 |
方法 | 说明 |
---|---|
public final Object readobject() | 把存储在文件中的Java对象读出来 |
ObjectOutputstream
构造器 | 说明 |
---|---|
public 0bjectOutputStream(OutputStream out) | 创建对象字节输出流,包装基础的字节输出流 |
方法 | 说明 |
---|---|
public final void write0bject(object o) throws IOException | 把对象写出去 |
注意
- 一个对象如果要序列化的话一定要去实现Serializable接口,并且不参与序列化的成员变量使用 transient修饰
- 存储的内容也不是为了显示的,而是为了再次读入
- 如果有多个对象需要序列化,直接用ArrayList集合存储多个对象之后,对集合进行序列化即可,ArrayList已经实现了Serializable接口
释放资源
try - catch -finally
InputStream is = null;
OutputStream os = null;
try{is = FileInputStream("1");os = FileOutputStream("2");...
}catch(IOException e){e.printStackTrace;
}finally{try{if(is!=null)is.close;}catch(IOException e){e.printStackTrace;}try{if(os!=null)os.close;}catch(IOException e){e.printStackTrace;}
}
try - with - resources
上面的代码比较臃肿
看看下面从的jdk7开始提供的方案
try(定义资源1;定义资源2;…){可能出现异常的代码;
}catch(异常类名变量名){异常的处理代码;
)
该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
InputStream is = null;
OutputStream os = null;
try(InputStream is = FileInputStream("1");OutputStream os = FileOutputStream("2");
){...
}catch(IOException e){e.printStackTrace;
}
**注意!:**在try的()当中必须是实现了AutoCloseAble接口的资源对象
总结
File主要就是和文件和文件夹相关的一些操作,构造器包含 相对路径or绝对路径;父文件or父文件路径、子文件名
UTF-8提供了一种万国码,支持可变长编码,1、2、3、4字节,中文处在3字节编码区
GBK是国标中文字符集,2字节定长
IO流是处理数据输入输出的,总共是四种接口,InputStream、OutputStream、Reader、Writer,分别是字节输入输出、字符输入输出
他的实现有多种:1、File**** 2、Buffered**** ;分别对应原始流和包装流,Buffered包装了缓冲流
还有一些其他的。比如转换流用于转换字符集,打印流方便输出,数据流同时输入输出类型+内容,
特殊文件
方便解析相对应的内容,可以存储有关系的数据,作为系统的配置,也可以作为信息进行传输
属性文件
每行都是不重复的键值对,使用.properties结尾
使用Properties读写属性文件当中的内容,Properties是一个Map的实现类
构造器 | 说明 |
---|---|
public Properties() | 用于构建Properties集合对象(空容器) |
常用方法 | 说明 |
---|---|
public void load(InputStream is) | 通过字节输入流,读取属性文件里的键值对数据 |
public void load(Reader reader) | 通过字符输入流,读取属性文件里的键值对数据 |
public String getProperty(String key) | 根据键获取值(其实就是get方法的效果) |
public Set <string> PropertyNames() | 获取全部键的集合(其实就是keySet方法的效果) |
可以用PropertyNames得到set进行遍历,也可以用forEach进行遍历
常用方法 | 说明 |
---|---|
public Object setProperty(String key, String value) | 保存键值对数据到Properties对象中去。 |
public void store(OutputStream os, String comments) | 把键值对数据,通过字节输出流写出到属性文件里去 |
public void store(Writer w, String comments) | 把键值对数据,通过字符输出流写出到属性文件里去 |
public class ConfigReflectTest {public static void main(String[] args) {try {// 1) 加载配置文件Properties prop = new Properties();try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("conf.properties")) {if (in == null) {throw new RuntimeException("找不到 conf.properties");}prop.load(in);}// 2) 反射创建 className 指定的对象String className = prop.getProperty("className");} catch (Exception e) {e.printStackTrace();}}
}
XML文件
本质是一种数据的格式,可以用来存储复杂的数据结构,和数据关系。
特点:
- XML中的"<标签名>”称为一个标签或一个元素,一般是成对出现的。
- XML中的标签名可以自己定义(可扩展),但必须要正确的嵌套。
- XML中只能有一个根标签。
- XML中的标签可以有属性。
- 如果一个文件中放置的是XML格式的数据,这个文件就是XML文件,后缀一般写成.xml
XML语法
-
XML文件的后缀名为:xml,文档声明必须是第一行
-
XML中可以定义注释信息:<!–注释内容->
-
XML中书写”<”、“&”等,可能会出现冲突,导致报错,此时可以用如下特殊字符替代。
| 内容 | 符号 | 含义 |
| ---- | ---- | ------ |
| < | < | 小于 |
| > | > | 大于 |
| & | & | 和号 |
| ' | ’ | 单引号 |
| " | " | 引号 | -
XML中可以写一个I叫CDATA的数据区:<[CDATA[…内容…]>,里面的内容可以随便写。
应用场景:经常用来做为系统的配置文件;或者作为一种特殊的数据结构,在网络中进行传输。
使用XML读写
以Dom4j为例,他通过SAXReader解析器解析xml成Document,之后解析Document如下:
具体方法如下:
SAXReader:Dom4j提供的解析器,可以认为是代表整个Dom4j框架
构造器/方法 | 说明 |
---|---|
public SAXReader() | 构建Dom4J的解析器对象 |
public Document read(String url) | 把xML文件读成Document对象 |
public Document read(InputStream is) | 通过字节输入流读取XML文件 |
Document
方法名 | 说明 |
---|---|
Element getRootElementO | 获得根元素对象 |
Element
方法名 | 说明 |
---|---|
public String getName() | 得到元素名字 |
public List<Elements> elements() | 得到当前元素下所有子元素 |
public List <Elements> elements(String name) | 得到当前元素下指定名字的子元素返回集合 |
public Element element(String name) | 得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个 |
public String attributeValue(String name) | 通过属性名直接得到属性值 |
public String elementText(子元素名) | 得到指定名称的子元素的文本 |
public String getText() | 得到文本 |
//3、从文档对象中解析XML文件的全部数据了
ELement root = document.getRootElement();
System.out.println(root.getName());
//4、获取根元素下的全部一级子元素。
List<Element>elements = root.elements();
List<Element>elements = root.elements("user");
for (Element element:elements){System.out.println(element.getName();
}
//5、获取当前元素下的某个子元素。
Element people = root.element("people");
System.out.println(people.getText());
//如果下面有很多子元素User,默认获取第一个。
Element user = root.element("user");
System.out.println(user.elementText("name"));
框架
框架是解决某类问题,编写的一套类、接口等,可以理解成一个半成品,大多框架都是第三方研发的。
**好处:**在框架的基础上开发,可以得到优秀的软件架构,并能提高开发效率
框架的形式:一般是把类、接口等编译成class形式,再压缩成一个jar结尾的文件发行出去。
IO框架
Commons-io
Commons-io是apache:开源基金组织提供的一组有关IO操作的小框架,目的是提高lO流的开发效率。
FileUtils类提供的部分方法展示 | 说明 |
---|---|
public static void copyFile(File srcFile, File destFile) | 复制文件。 |
public static void copyDirectory(File srcDir, File destDir) | 复制文件夹 |
public static void deleteDirectory(File directory) | 删除文件夹 |
public static String readFileTostring(File file, String encoding) | 读数据 |
public static void writeStringToFile(File file, String data, String charname, boolean append) | 写数据 |
IOUtils类提供的部分方法展示 | 说明 |
---|---|
public static int copy(InputStream inputStream, OutputStream outputStream) | 复制文件。 |
public static int copy(Reader reader, Writer writer) | 复制文件。 |
public static void write(String data, OutputStream output, String charsetName) | 写数据 |
日志技术
记录程序运行过程中的各种信息,可以将系统执行的信息,方便的记录到指定的位置(控制台、文件中、数据库中)。
可以随时以开关的形式控制日志的启停,无需侵入到源代码中去进行修改。
日志框架:牛人或者第三方公司已经做好的实现代码,后来者直接可以拿去使用。
日志接口:设计日志框架的一套标准,日志框架需要实现这些接口。
框架有JUL、Log4j、Logback等
接口有:Commins Logging(JCL);Simple Logging Facade for Java (SLF4J)
Logback目前是最流行的,他分为三个模块:logback-core(基础模块)、logback-classic(实现了老的Log4J)、logback-access(日志访问)
要使用Logback,则必须有三个模块进行整合slf4j-api、logback-core、logback-classic
使用的话,在项目中加入三个jar包,并add as library,将对应的核心配置文件放在src目录下
核心配置文件控制层级输出等
网络编程
通信三要素
-
①IP地址
要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识
-
② 端口
网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识
-
③协议
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议
InetAddress
在Java.net当中
IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志。
有IPv4(32位)和IPv6(64位)
-
ipv4
由一个32位的二进制数据组成,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP 地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”。
② -
ipv6
采用128位地址长度,每16个字节一组,分成8组十六进制数。
Java使用InetAddress类代表IP地址。
他的常用方法如下:
名称 | 说明 |
---|---|
public static InetAddress getLocalHost() | 获取本机IP,会以一个inetAddress的对象返回 |
public static InetAddress getByName(String host) | 根据ip地址或者域名,返回一个inetAdress对象 |
public String getHostName() | 获取该ip地址对象对应的主机名。 |
public String getHostAddress() | 获取该ip地址对象中的ip地址信息。 |
public boolean isReachable(int timeout) | 在指定毫秒内,判断主机与该ip对应的主机是否能连通 |
UDP通信效率高,不建立连接,但是不可靠,可能丢包,丢了就丢了。数据包在64KB内。
TCP通信效率低,建立连接,但是可靠,丢包会重传
UDP通信
特点:无连接、不可靠通信。
不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端1P、等信息封装成一个数据包,发出去就不管了。
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) | 创建发出去的数据包对象 |
public DatagramPacket(byte[] buf, int length) | 创建用来接收数据的数据包 |
方法 | 说明 |
---|---|
public int getLength() | 获取数据包,实际接收到的字节个数 |
客户端
public class Client {public static void main(String[] args) throws IOException {DatagramSocket socket = new DatagramSocket();byte[] buf ;Scanner in = new Scanner(System.in);while(true){System.out.println("Enter command: ");String line = in.nextLine();buf = line.getBytes();if("exit".equalsIgnoreCase(line)){System.out.println("Exit");socket.close();break;}DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(),6666);socket.send(packet);System.out.println("数据发送完毕");}}
}
服务端
public class Server {public static void main(String[] args) throws IOException {DatagramSocket socket = new DatagramSocket(6666);byte[] buf = new byte[1024*64];DatagramPacket packet = new DatagramPacket(buf, buf.length);while(true) {socket.receive(packet);int length = packet.getLength();buf = packet.getData();System.out.println(new String(buf, 0, length));System.out.println("````````数据接收成功````````");}}
}
TCP通信
特点:面向连接、可靠通信。
通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
Java提供了一个java.net.Socket类来实现TCP通信。
构造器 | 说明 |
---|---|
public Socket(String host , int port) | 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket |
方法 | 说明 |
---|---|
public OutputStream getOutputStream() | 获得字节输出流对象 |
public InputStream getInputStream() | 获得字节输入流对象 |
服务端是通过java.net包下的ServerSocket类来实现的。
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 为服务端程序注册端口 |
方法 | 说明 |
---|---|
public Socket accept() | 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。 |
客户端:
class TCP_Client {public static void main(String[] args) {try (Socket socket = new Socket("127.0.0.1", 8888);DataOutputStream out = new DataOutputStream(socket.getOutputStream());Scanner in = new Scanner(System.in);) {while (true) {System.out.println("Enter your words: ");String input = in.nextLine();if("exit".equalsIgnoreCase(input)) {System.out.println("欢迎下次使用");break;}out.writeUTF(input);out.flush();System.out.println("输出成功");}} catch (Exception e) {e.printStackTrace();}}
}
服务端想要实现和多个客户端进行响应有很多种方法
这里我们使用多线程,主线程接收到一个socket之后,交给一个线程处理一个socket的内容。
public class TCP_Server_Thread implements Runnable {Socket socket;public TCP_Server_Thread(Socket socket) {this.socket = socket;}public TCP_Server_Thread() {}@Overridepublic void run() {try (DataInputStream din = new DataInputStream(socket.getInputStream());) {while (true) {String msg = null;try {msg = din.readUTF();System.out.println(msg);System.out.println("```接收成功```");} catch (IOException e) {System.out.println("客户端"+socket.getRemoteSocketAddress()+"下线了");// 手动关闭Socketsocket.close();break;}}} catch (RuntimeException | IOException e) {throw new RuntimeException(e);}}
}
public class TCP_Server {public static void main(String[] args) {try (ServerSocket ss = new ServerSocket(8888);) {while (true) {// 注意这里不能把Socket放在()中进行处理,因为接收完会自动close,这个Socket就不能使用了try {Socket s = ss.accept();System.out.println("Accepted connection from " + s.getRemoteSocketAddress());new Thread(new TCP_Server_Thread(s)).start();} catch (Exception e) {e.printStackTrace();}}} catch (Exception e) {e.printStackTrace();}}
}
案例
更多案例查看实践部分
格外注意的点是:
- 对于BS架构,服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据
- 最好不要将Socket放在try(当中){},因为会自动close(),最好手动关闭Socket。
单元测试
就是针对最小的功能单元(方法),编写测试代码对其进行正确性测试。
Junit单元测试框架:可以用来对方法进行测试,它是第三方公司开源出来的(很多开发工具已经集成了Junit框架,比如IDEA)
可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
具体步骤
- ①将unit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
- ②为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
- ③测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法进行测试;
- ④开始测试:选中测试方法,右键选择“Unit运行”,如果测试通过则是绿色;如果测试失败,则是红色
提供了断言机制来进行业务方法的正确性预测比如:
Assert.assertEquals(message, expected, actual)
可以在项目中run all test一键测试所有测试方法
其他注解:
@Before在每一个测试方法之前执行一次:
@After在每一个测试方法之后执行一次
@BeforeClass在所有测试方法执行之前,仅执行一次
@AfterClass在所有测试方法执行之后,仅执行一次
在JUnit5之后修改了名字:@BeforeEach、@AfterEach、@BeforeAll、@AfterAll
反射、注解、动态代理
反射
反射就是:加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等)。
可以破坏封装类
适合做框架
1、加载类,获取类的字节码:Class对象
2、获取类的构造器:Constructor对象
3、获取类的成员变量:Field对象
4、获取类的成员方法:Method对象
获取字节码:Class对象
- Class c1=类名.class
- 调用Class提供方法:public static Class forName(String package);
- Object提供的方法:public Class getclass();Class c3=对象.getClass();
Class 对象有getName方法得到全类名,getSimpleName得到简称。
获取构造器:Constructor对象
Class提供了从类中获取构造器的方法。
方法 | 说明 |
---|---|
Constructor<?>[] getconstructors() | 获取全部构造器(只能获取public修饰的) |
Constructor<?>[] getDeclaredconstructors() | 获取全部构造器(只要存在就能拿到) |
Constructor<T> getConstructor(Class<?>…parameterTypes) | 获取某个构造器(只能获取public修饰的) |
Constructor<T> getDeclaredConstructor(Class<?>…parameterTypes) | 获取某个构造器(只要存在就能拿到) |
Class c = Student.class;// 得到无参构造器
Constructor constructor1 = c.getDeclaredConstructor();
// 得到指定参数的构造器
Constructor constructor2 = c.getDeclaredConstructor(String.class, int.class);
获取构造器对象的作用是:初始化对象返回
Constructor提供的方法 | 说明 |
---|---|
T newInstance(object…initargs) | 调用此构造器对象表示的构造器,并传入参数,完成对象的初始化并返回 |
public void setAccessible(boolean flag) | 设置为true,表示禁止检查访问控制(暴力反射,即使是private也可以调用) |
因此,反射其实是会破坏封装性的。
// 强制可以访问
constructor2.setAccessible(true);constructor2.newInstance("张三",3);
获取成员变量:Field对象
Class提供了从类中获取成员变量的方法。
方法 | 说明 |
---|---|
public Field[] getFields() | 获取类的全部成员变量(只能获取public修饰的) |
public Field[] getDeclaredFields() | 获取类的全部成员变量(只要存在就能拿到) |
public Field getField(String name) | 获取类的某个成员变量(只能获取public修饰的) |
public Field getDeclaredField(String name) | 获取类的某个成员变量(只要存在就能拿到) |
作用依然是赋值和取值
方法 | 说明 |
---|---|
void set(Object obj, Object value): | 赋值 |
Object get(object obj) | 取值 |
public void setAccessible(boolean flag) | 设置为true,表示禁止检查访问控制(暴力反射) |
student s = (student) constructor2.newInstance("张三",3);
fName = c.getDeclaredField("name");fName.setAccessible(true);
fName.set(s, "李四");
获取成员方法:Method对象
Class提供了从类中获取成员方法的方法。
方法 | 说明 |
---|---|
Method[] getMethods() | 获取类的全部成员方法 (只能获取public修饰的) |
Method[] getDeclaredMethods() | 获取类的全部成员方法 (只要存在就能拿到) |
Method getMethod(String name, Class<?> … parameterTypes) | 获取类的某个成员方法 (只能获取public修饰的) |
Method getDeclaredMethod(String name, Class<?> … parameterTypes) | 获取类的某个成员方法 (只要存在就能拿到) |
反射得到的方法作用依然是执行
Method提供的方法 | 说明 |
---|---|
public Object invoke(Object obj, Object… args) | 触发某个对象的该方法执行。 |
public void setAccessible(boolean flag) | 设置为true,表示禁止检查访问控制(暴力反射) |
student s = (student) constructor2.newInstance("张三",3);
// 得到午餐run方法
Method m0 = c.getDeclaredMethod("run");
// 得到有两个int参数的run方法
Method m1 = c.getDeclaredMethod("run", int.class, int.class);// 调用实例s的无参run方法
m0.invoke(s);m1.setAccessible(true);
// 调用实例s的两个int参数的run方法
m1.invoke(s,2,3);
注解
注解就是Java代码里的特殊标记,比如:@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行该程序。
注解可以用在类上、构造器上、方法上、成员变量上、参数上、等位置处。
自定义注解
形式如下:
public @interface 注解名称{public 属性类型 属性名称() default 默认值;
}
例如
public @interface MyAnnotation{public int age() default 18;public string[] friends();
}
使用时:
@MyAnnotation(friends = {"小六"})
public class Mytest{@MyAnnotation(age = 1,friends= {"张三", "李四", "王五"})public void test(){}
}
特别的,当注解只有一个属性,且这个属性为value的时候,可以省略 value = 部分直接使用 @MyAnnotation(“属性值”)
注解的原理
他的本质是一个继承了Annotation接口的接口,里面的属性其实都是抽象方法。
使用的注解,其实是一个注解接口的实现类对象。会封装对应的属性信息,可以调用对应的方法来获取对应的属性值。
元注解
用于修饰注解的注解。主要有两个:@Target和@Retention
@Target
作用:声明被修饰的注解只能在哪些位置使用
@Target(ElementType.TYPE)
- 1.TYPE,类,接口
- 2.FIELD,成员变量
- 3.METHOD,成员方法
- 4.PARAMETER,方法参数
- 5.CONSTRUCTOR,构造器
- 6.LOCAL_VARIABLE,局部变量
@Retention
作用:声明注解的保留周期。
@Retention(RetentionPolicy.RUNTIME)
- 1.SOURCE:只作用在源码阶段,字节码文件中不存在。
- 2.CLASS(默认值):保留到字节码文件阶段,运行阶段不存在。
- 3.RUNTIME(开发常用):一直保留到运行阶段。
注解的解析
注解的解析,就是判断类上、方法上、成员变量上是否存在注解,并把注解里的内容给解析出来。
指导思想:要解析谁上面的注解,就应该先拿到谁。
比如要解析类上面的注解,则应该先获取该类的Class对象,再通过Class对象解析其上面的注解。
比如要解析成员方法上的注解,则应该获取到该成员方法的Method对象,再通过Method对象解析其上面的注解。
Class、Method、Field、Constructor、都实现了AnnotatedElement接口,它们都拥有解析注解的能力。
AnnotatedElement接口提供了解析注解的方法 | 说明 |
---|---|
D public Annotation[] getDeclaredAnnotations() | 获取当前对象上面的注解。 |
public T getDeclaredAnnotation(Class<T> annotationClass) | 获取指定的注解对象 |
public boolean isAnnotationPresent(Class<Annotation> annotationClass) | 判断当前对象上是否存在某个注解 |
结合前面的案例内容
Class c_stu = Mytest.class;
Method m_stu = c_stu.getDeclaredMethod("test");if(c_stu.isAnnotationPresent(MyAnnotation.class)){MyAnnotation ma = (MyAnnotation) c_stu.getDeclaredAnnotation(MyAnnotation.class);sout(ma.age());
}if(m_stu.isAnnotationPresent(MyAnnotation.class)){MyAnnotation ma = (MyAnnotation) m_stu.getDeclaredAnnotation(MyAnnotation.class);sout(ma.age());
}
应用场景
注解往往结合反射用于框架的构建,下面给出一个应用:实现添加了@Mytest注解的方法进行运行测试
思路:使用main方法运行,通过反射得到类下面的所有方法。检查是否被@Mytest注解修饰。修饰了则调用invoke进行方法调用
public class MyUnit{public void test1(){sout("输出1");}@Mytestpublic void test2(){sout("输出2");}public void test3(){sout("输出3");}public void test4(){sout("输出4");}@Mytestpublic void test5(){sout("输出5");}public static void main(){MyUnit mu = new MyUnit();class c = MyUnit.class;Method[] ms = c.getDeclaredMethods();for(Method method:ms){if(method.isAnnotationPresent(MyTest.class)){// 使用invoke的时候别忘了需要传入一个调用该方法的实例对象,以及参数,此处无参method.invoke(mu);}}}
}
动态代理
对象如果嫌身上干的事太多的话,可以通过代理来转移部分职责。
对象有什么方法想被代理,代理就一定要有对应的方法。
生成代理的前提是,具体的对象需要实现一个接口,这个接口包含了所有需要进行代理的方法。
如下:
public interface Star{String sing(String name);void dance();
}
创建代理一般都是使用Proxy.newProxyInstance方法,需要传入三个参数
- ClassLoader loader
- 用于指定类加载器,用这个类加载器去加载生成的代理类。
- 一般都是用当前的类的类加载器
- Class<?>[] interfaces
- 指定生成的代理长什么样子,有哪些方法。可以有多个接口。
- 代理在创建后使用 “.” 进行调用,可以调用哪些方法是在这里定义的。但是实际执行是由下面的Invoke方法执行的。
- InvocationHandler h
- 这是一个函数式接口,有一个Invoke方法
- 这个Invoke方法是代理进行方法调用的时候会实际执行的回调方法
- Invoke方法会传入三个参数:Object proxy、Method method、Object[] args 分别代表
- 用来指定生成的代理对象要干什么事情
- 一般采用匿名内部类实现
- 这是一个函数式接口,有一个Invoke方法