javase 数组
四、数组
数组概述
- 数组是引用数据类型,隐式继承Object,因此可以调用Object类中的方法
- 数组对象存储在堆内存中
数组的特点
- 数组长度一旦确定不可改变
- 所有数组对象都有length属性,用来获取数组元素个数
优点:
- 根据下标查找某个元素的效率极高
缺点:
- 随机增删的效率低,需要后移/前移很多元素
- 无法存储大量数据,因为很难在内存上找到非常大的一块连续内存
一维数组
静态初始化一维数组
已经知道数组中的值时使用
1 | // 第一种 |
用第一种就好了!
JDK5 新特性:增强for循环 / for-each 循环
1 | for(元素数据类型 变量名:数组名){ // 变量名代表数组中的每个元素,可以自己取名 |
优点:代码简洁
缺点:没有下标
动态初始化
不知道数组中具体存储哪些元素时使用
1 | 数据类型[] 变量名 = new 数据类型[长度] |
数组长度确定,数组中存储的每个元素将采用默认值。
数组中如何存储不同类的对象
创建父类类型的数组,即可存子类的对象
eg.
1 | Apple a = new Apple(); |
存的是对象的地址
关于main方法的形参args
作用:接收命令行参数用的
JVM负责调用main方法时用的 ——JVM负责给main方法准备一个String[ ]一维数组对象
java fileName abc def xyz
命令行参数:abc def xyz,JVM会将命令行参数以空格进行拆分,生成一个新的数组对象。String[ ] args = {“abc”,”def”,”xyz”};
命令行参数有什么用?
需求:使用该系统的时候,需要提供正确的口令(用户名和密码),非法用户直接退出系统。
当两个字符串进行equals比较时,如果其中有一个字符串是字面量,建议将字面量写到前面。即:"string".equals(variable)
,避免出现空指针异常。
可变长度的参数
function1(int ... nums)
- 语法格式:
数据类型...
- 在形参列表中,可变长度的参数只能有一个,且只能在参数列表的末尾
- 可变长度的参数可以当做数组来看待 可通过这种方式访问:nums[0],nums[1]
一维数组的扩容
数组长度一旦确定不可改变
只能新建一个更大的数组,然后将原数组的数据全部拷贝到新数组中,可以使用
System.arraycopy()
数组扩容会影响程序的执行效率,因此尽可能预测数据量,减小扩容次数。
二维数组
静态初始化
1 | int[][] arr = new int [][]{{1,2,3},{1,2,3,4,5},{1}}; |
动态初始化
1 | // 等长 |
Arrays 工具类
- 自定义类型做比较的话,这个自定义类型必须实现Comparable接口,并实现compareTo方法。使用sort进行排序时也需要实现该方法。
int[] Arrays.copyOf()
是系统自动在内部新建一个数组,将原来的数组复制到新建的数组中,并返回新建的数组。而System.arraycopy()
没有新建数组,是直接将内容复制到另一个数组中的。而且arraycopy是native方法,由c++代码实现,因此arraycopy的拷贝速度更快。
五、异常
什么是异常?
java程序执行过程中的意外、错误、不正确的情况
异常在java中的形式
以类和对象的形式存在。
定义异常其实本质上就是定义一个类。
异常如果发生的话,在底层其实通过了这个类new了一个对象。
自定义异常
- 自定义异常的类需要继承Exception或者RuntimeException,如果继承的是Exception就认为这个异常是编译时异常。
- 提供两个构造方法,一个是无参数的,一个是带有String参数的,并且在构造方法中调用
super(String);
处理异常的两种方法
抛出异常:在类的声明中添加
throws 异常类名
如果有些方法不允许使用thorws,也可以使用try catch然后在catch里面使用throw
捕捉异常
1 | try{ |
注:异常类型1,2,3,… 一定是从小到大的,否则如果第一个就是父类,那永远都不可能运行后面的异常处理代码了。
throw和throws的区别:
- throw是运行时的语句,真正地抛出一个异常实例
- throws是编译时的声明,告诉编译器和调用者,如果出现这些问题就抛出。
JAVA7 新特性 —— 异常统一处理方式
1.
1 | try{ |
异常对象的方法
- getMessage
- printStackTrace
1 | try{ |
异常的堆栈信息:
异常信息的打印是符合栈这个数据结构的,因此优先看最上面的异常行数,最上面是最后执行的代码
打印异常堆栈信息可能出现在“后面执行的代码”前面,也可能在后面。因为高版本的底层是用多线程并行打印的。
finally 语句块
放在该语句块中的代码是一定会执行的(无论前面的程序是否有异常),一般在finally语句块中完成资源的释放。
顺序:try…(catch)…finally
继承问题
子类继承父类后,重写了父类的方法,重写之后不能抛出更多的异常,可以更少。
六、常用类
字符串 String
为什么string 字面量不可变?
因为底层代码中string是用byte数组存的,而byte数组是private final修饰的,因此无法修改它的值。(java8及之前是char数组)
字符串拼接
- 如果拼接的两个字符串中有一个是变量,那么拼接后的新字符串不会放到字符串常量池中。而是在堆中。
底层在进行拼接时,会创建一个StringBuilder对象,进行字符串拼接。最后自动调用StringBuilder对象中的toString()
方法,再将StringBuilder对象转换成String对象。
- 两个字符串字面量拼接会在编译阶段做优化,在编译阶段进行拼接(可以这么理解,但不准确)因此字符串常量池中只有拼接后的内容。
怎么把字符串手动放进字符串常量池?
1 | String m = "test"; |
只能加东西,不能删东西。
String类常用的构造方法
String常用方法
正则表达式
String 中正则表达式相关的方法
StringBuffer 与 StringBuilder 可变长度字符串
- 这两个类是专门为频繁进行字符串拼接而准备的
- StringBuffer是先出现的,Java5时新增了StringBuilder。StringBuffer是线程安全的,而StringBuilder效率更高。
- 两者底层都是byte[]数组,并且没有被final修饰,因此可以扩容。
- 优化策略:创建对象时预估好字符串的长度,给定一个合适的初始化容量,减少底层数组扩容的次数。
- StringBuilder默认初始化容量:16
- StringBuilder扩容策略:每次扩容为原来的两倍+2
为什么频繁拼接字符串时使用StringBuilder/StringBuffer更好?
使用“+”进行拼接,底层每次都会创建一个StringBuilder对象,然后再调用toString方法,10000次拼接就要创建10000次对象,同时给垃圾回收也造成了很大的压力。
而StringBuilder的append不创建新对象,直接在原来的位置进行拼接,且不调用toString方法,只有用print输出的时候才调用一次,因此节省了大量的时间。
包装类
包装类中的6个数字类型都继承了Number类
装箱boxing:将基本数据类型包装成引用数据类型 Integer i = new Integer(100);
拆箱:int num = i.intValue()
Integer 常用方法
String、int、Integer 三者相互转换
自动装箱/拆箱(JAVA5新特性)
编译阶段的功能,底层仍然是之前的装箱/拆箱。只是让你编程的时候方便一点。
自动装箱
Integer x = 100;
自动拆箱
int num = x;
整数型常量池
[-128~127]这些数字太常用了,为了提高效率,Java提供了一个整数型常量池。
这个常量池是一个数组:Integer[ ] integerCache;
数组中存储了256个Integer的引用,只要没有超出这个范围的数字,直接从整数型常量池中取。
BigInteger
大数字:
- 超过long了使用java.math.BigInteger
- 他的父类是Number
- 他是引用数据类型
常用方法:
BigDecimal
浮点型超过double就使用BigDecimal
构造方法:BigDecimal(String val)
常用方法:
DecimalFormat
该类是专门用来对数字进行格式化的。
常用数字格式:
- ###,###.## 三个数字为一组,组和组之间使用逗号隔开,保留两位小数
- ###,###.0000 三个数字为一组,组和组之间使用逗号隔开,保留4位小数,不够补0
构造方法:DecimalFormat(String pattern)
常用方法:String format(数字)
日期相关API
获取时间
1 | // java.util.Date 日期API |
日期格式化
1 | import java.util.Date; |
将String转化成Date
1 | String strDate = "2008-08-08 08:08:08 888"; |
java.util.Calend ar 日历类
获取当前时间的日历对象
1 | Calendar c = Calendar.getInstance(); |
获取日历中的某部分
修改日历中的内容
日历的新API(java8)
日期
传统的日期API存在线程安全问题,于是java8提供了一套全新的日期API
时间戳
计算时间间隔、日期间隔
时间矫正器
日期格式化
注意:这里使用LocalDateTime去调用parse方法,还需要把格式作为参数传入。
数学类 Math
回顾:工具类的方法都是静态的,直接使用类名调用。
枚举类
优点
- 可读性强
- 做了类型的限定,在编译阶段就可以确定类型是否正确,不正确会报错
定义
1 | enum 枚举类型名 { |
特点:
高级用法
Random 随机数生成器
1 | Random random = new Random(); |
System类
UUID 通用唯一标识符
UUID是一种软件构建的标准,用来生成具有唯一性的ID。
1 | UUID uuid = UUID.randomUUID(); |
七、集合
集合概述
集合是一种容器,用来组织和管理数据。
Java的集合框架对应的这套类库其实就是对各种数据结构的实现。
集合存储的是引用。
默认情况下,如果不使用泛型,集合中可以存储任何类型的引用。
Java集合框架分为两部分:
- Collection结构:元素以单个的形式存储
- Map结构:元素以键值对的映射关系存储
Collection 关系图
Collection接口的通用方法
Collection的通用遍历/迭代方式
面向接口编程
1 | Collection col = new ArrayList(); |
SequencedCollection接口
所有的有序集合都实现了SequencedCollection接口
泛型
- java5新特性,是编译阶段的功能。
泛型初体验
- 程序编写时看帮助文档中是否有”<>”符号,如果有这个符号就可以使用泛型。
- 创建一个集合,要求这个集合中只能存放某种类型的对象,就可以使用泛型
1 | Collection<User> users = new ArrayList<User>(); |
泛型的作用
钻石表达式 (Java7新特性)
1 | Collection<User> users = new ArrayList<>() // 后面尖括号中的内容可以省略 |
泛型擦除与补偿(了解)
泛型的定义
在类上自定义泛型
1 | public class vip<NameType, AgeType>{ // 在声明类时写上泛型名称 |
在类上定义的泛型,在静态方法中无法使用。(因为静态方法直接通过类名调用,此时还没有通过声明类的对象来指定泛型的类型。)
在静态方法上定义泛型
1 | public class test{ |
在接口上定义泛型
和类定义泛型差不多。
1 | public interface MyCompare<T>{ |
泛型的使用
泛型通配符
无限定通配符
<?>
此处表示后面填写的泛型可以是任意数据类型。
上限通配符
<? extends Number>
表示泛型必须为Number及其子类
下限通配符
<? super Number>
表示泛型必须为Number及其父类
集合的并发修改问题 fail-fast 机制
集合中设置了一个modCount属性,用来记录修改的次数,使用集合对象执行增删改的操作时,modCount就会自动加1。
获取迭代器对象时,会给迭代器对象初始化一个expectedModCount属性,并且将modCount的值赋值给expectedModCount。
即
int expectedModCount = modCount;
当使用集合对象删除元素时,modCount会加1,但是迭代器中的expectedModCount没有加1。而当迭代起对象的
next()
方法执行时,会检测expectedModCount和modCount是否相等,如果不相等,就会抛出ConcurrentModificationException异常而如果使用迭代起删除元素时,modCount和expectedModCount都会加1.这样next()方法在检测时就是相等的,不会出现异常。
注:即使没有使用多线程编程,但是用迭代器去遍历的同时使用集合去删除元素,这个行为将被认为并发修改。
所以,迭代集合时,要使用 迭代器对象.remove()
,移除的是当前光标所执行的元素。
List 接口
特点
有序、可重复
常见的实现类
- ArrayList 数组
- Vector、Stack 数组(线程安全的)
- LinkedList 双向链表
List接口特有的方法
List特有的迭代方式
注:调用迭代器的remove和set方法的前提是之前调用了next或者previous方法获取了一个元素,remove和set是作用于之前获取的那个元素上的。
List接口使用Comparator排序
回顾数组中自定义类型是如何排序的?
- 所有自定义类型排序时必须指定排序规则,实现Comparable接口,并重写compareTo方法。 重写是override
List集合的排序
- default void sort(Comparator<? super E> c);
- sort方法需要一个参数:java.util.Comparator ,我们把它叫做比较器,它是一个接口。
- 如何给自定义类型指定比较规则?可以对Comparator提供一个实现类,并重写compare方法来指定比较规则
- 这个实现类也可以看采用匿名内部类的方式。
对数组的排序是在类里面重写比较规则,对List集合的排序是单独设定一个比较规则并在需要时使用。
ArrayList 类
回顾:数组的优缺点
优点
数组在内存中是连续存储的,有下标就有偏移量,可以通过偏移量计算出对应元素的内存地址。检索效率高,时间复杂度O(1)
缺点
- 不能存储大数据(因为内存地址是连续的)
- 随机增删元素耗时很长
使用场景
需要频繁检索元素,很少进行随机增删的情况。
ArrrayList扩容策略
- 当调用无参构造方法时,初始化容量为0。
- 当第一次调用add方法时,将ArrayList容量初始化为10个长度。
- 后续扩容时,底层会创建一个新的数组,然后使用数组拷贝。新数组的容量是原容量的1.5倍。
Vector 类(*不怎么使用了)
LinkedList 双向链表类
栈 数据结构
队列 数据结构
入队:offer
出队:poll
三种Set
map和set的关系
map是键值对,把键那一列单独拿出来,就是set集合。
Map
Map 接口的常用方法
Map 集合的遍历
方法一:获取Map集合的所有key,然后遍历每个key,通过key获取value
1 | Set<Integer> keys = maps.keySet(); |
方法二:获取Map的内部类Map.Entry (效率更高,常用这个)
不需要再通过key去找value了
1 | Set<Map.Entry<Integer,String>> entries = maps.entrySet(); |
HashMap
哈希表存储原理
!!hashCode和equals方法要同时重写
使用equals的前提条件是两个元素计算得到的索引值是相同的,在同一个链表中。那么保证这两个元素使用hashCode()返回的结果是相同的才能准确的保证索引值相同。
因此,存放在HashMap集合key部分的元素,以及存放在HashSet集合中的元素,需要同时重写hashCode和equals方法
HashMap在Java8后的改进
初始化时机
java8之前,构造方法执行初始化table数组
java8之后,第一次调用put方法时初始化table数组
插入方法
java8之前:头插法
java8之后:尾插法
数据结构
java8之前:数组+单向链表
java8之后:数组+单向链表/红黑树
- 如果结点数量>=8,且table长度>=64,单向链表转为红黑树
- 当删除红黑树上的结点,使节点数量<=6时,红黑树转换为单向链表
HashMap的容量永远是2的次幂
原因:
提高哈希计算的效率(位运算的效率比%取模运算效率高)
当length为2的次幂时,length-1的二进制低位全是1,此时
hash & (length - 1)
相当于 保留hash
的低n
位,结果与hash%length
一致,使用位运算效率更高。减少哈希冲突,让散列分布更加均匀
假设length是偶数,length-1结果一定是奇数,它的二进制中的最后一位一定是1,和别人相与可能是0或1。如果length是奇数,length-1是偶数,那么二进制最后一位是0,和别人相与只能是0,那么最后table有一半都是空的,存不了东西。
HashMap的初始化容量设置
- 当哈希表中的元素越来越多时,散列碰撞的几率就会越来越高,导致单链表过长,降低了哈希表的性能,此时要进行哈希表扩容
- 而一旦进行扩容,由于length改变,所有元素的hash值都会改变,效率比较低,所以在初始化的时候最好设置好数组大小,避免过多次数的扩容。
- 扩容时间点:当哈希表中的元素个数超过
数组大小*0.75
后进行扩容,新数组大小为2*原数组大小
LinkedHashMap
- LinkedHashMap是HashMap集合的子类
- 用法和HashMap几乎一样
- 只不过LinkedHashMap可以保证元素的插入顺序
- 底层数据结构:哈希表+双向链表(记录顺序)
Hashtable(效率低,不常用)
Properties 属性类
TreeMap
排序二叉树
按照左小右大存储,按照中序遍历自动得到升序排列的元素。
缺点:如果插入的节点集本来就是有序的,那么最后得到的二叉树其实就是一个普通链表,检索效率很差。
平衡二叉树
红黑二叉树
一棵自平衡的排序二叉树
构造方法
一个是没有参数的,一个是需要传比较器的
put() 方法
先调用比较器,如果比较器是NULL,就使用类中的compareTo方法进行比较。
因此有两种方式来修改比较方法。
法一:实现Comparable<>接口,并重写compareTo方法
适用于比较规则不会改变的情况,比如数字、字符串的比较
法二:再写一个类去实现Comparator<>接口,重写compare方法,在创建对象时将比较器传递给TreeMap
适用于比较规则会改变的情况
总结:哪些集合不能添加NULL
Hashtable的key、value
Properties的key、value
TreeMap的key
-> TreeSet不能添加null
Collections 工具类
八、IO流
IO流概述
分类
根据流向分
输入流(read)、输出流(write)
根据读写数据的形式分
字节流:一次读取一个字节。适合读取非文本数据,比如图片、音频、视频等。
字符流:一次读取一个字符。只适合读取普通文本,不适合读取二进制文件。因为字符流统一使用Unicode编码,可以避免出现编码混乱的问题。
根据流在IO操作中的作用和实现方式分
- 节点流:负责数据源和数据目的地的连接,是IO中最基本的组成部分。
- 处理流:处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理。
IO流体系结构
InputStream 字节输入流
OutputStream 字节输出流
Reader 字符输入流
Writer 字符输出流
所有流都实现了Closable接口,都有
close()
方法,流用完要关闭。所有的输出流都实现了Flushable,都有
flush()
方法,flush方法的作用是,将缓存全部写出并清空。
FileInputStream 类
称为文件字节输入流,是一个万能流,任何文件都能读,但还是建议读二进制文件,例如图片、声音、视频。
常用构造方法
FileInputStream(String name)
通过文件路径构建一个文件字节输入流对象。
注意: 反斜杠需要使用转义字符,即两个反斜杠 \\
也可使用一个正的斜杠 /
使用方法
int read();
调用一次read()方法就读取一个字节,返回读到的字节本身。如果读不到任何数据则返回-1
int read(byte[] b);
一次最多可以读到b.length个字节(只要文件内容足够多),返回值是读取到的字节数。读取的内容存在b数组中。
int read(byte[] b, int off, int len);
一次读取len个字节,将读到的数据从byte数组的off位置开始放
long skip(long n);
跳过n个字节
int available();
获取流中剩余的预估计字节数。
可以用这个初始化数组长度,这样就不需要使用循环来判断是否还有可读取的内容。
void close();
关闭流
FileOutputStream
文件字节输出流,负责写。
常用构造方法
- FileOutputStream(String name) 创建一个文件字节输出流对象,这个流在使用时,会先将原文件内容全部清空,然后写入。
- FileOutputStream(String name, boolean append)
创建一个文件字节输出流对象,当append是true时,不会清空原文件的内容,在原文件末尾追加。
当append是false时,会清空原文件的内容,在原文件末尾追加。
常用方法
void close();
void flush();
刷新
void write(int b);
写一个字节
void write(byte[] b);
将整个byte字节数组写入
void write(byte[] b, int off, int len);
将byte字节数组的一部分写入
TryWithResources 资源自动关闭 Java7新特性
凡是实现了AutoCloseable接口的流都可以使用try-with-resources,都会自动关闭。
格式:
1 | try( |
FileReader 读取普通文本
FileWriter
注意:只能复制普通文本文件!!!
路径
绝对路径、相对路径、类路径
1 | String path = Thread.currentThread().getContextClassLoader().getResource("filename").getPath(); |
Thread.currentThread()
获取当前线程
Thread.currentThread().getContextClassLoader()
获取当前线程的类加载器
getResource("filename")
从类的根路径下开始加载资源
src文件夹是类路径的根路径
优点:通用,在进行系统移植的时候,仍然可以使用。
注:这种方式只能从类路径中加载资源,如果这个资源在类路径之外,就无法访问到。
BufferedInputStream/BufferedOutputStream
对缓冲流的理解
使用
1️⃣为什么这里仍然需要使用数组呢?
这个数组是接收缓冲区中的大数组中的内容,它本身不和文件进行交互。
标记
mark()
在当前位置打上标记
reset()
回到上一次打标记的位置
一个文件中最多只有一个标记
调用顺序:先调用mark,再调用reset
如何解决乱码问题
所有输入输出底层都需要使用字节流,而字符流是将字节流包装后得到的。进行了这种包装操作的流叫包装流。
使用InputStreamReader/OutputStreamWriter时可以指定解码的字符集。
常用构造方法:
InputStreamReader(InputStream in)
采用平台默认的字符集进行解码InputStreamReader(InputStream in, String charsetName)
采用指定的字符集进行解码
FileReader是InputStreamReader的子类,是一个包装流。
FileWriter同理。
- InputStreamReader/OutputStreamWriter 的创建需要传入字节流,而FileReader/FileWriter 的创建直接输入文件地址即可。
数据流
将java程序中的数据直接写入文件,写进去就是二进制。
效率很高——写的过程不用转码
DataOutputStream写到文件中的数据,只能由DataInputStream来读取
读取顺序必须按照写入顺序!
1 | // 写入 |
对象的序列化与反序列化
序列化:将对象变成二进制文件
1 | ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("filename")); |
反序列化:将字节序列转换成JVM中的java对象
1 | ObjectInputStream ois = new ObjectInputStream(new FileInputStream("filename")); |
如果是多个对象,那就把这些对象放在集合中。
要参与序列化与反序列化的对象,必须实现 java.io.Serializable
接口。该接口是一个标志接口,没有任何方法。
- ObjectOutputStream也有关于数据输出的方法,比如
writeInt()
、writeBoolean()
等,和DataOutputStream中的方法一样。
序列化版本号
为了保证序列化的安全,只有同一个类的对象才能序列化和反序列化。在java中 通过 类名 + 序列化版本号(serialVersionUID)来判断。
当类的内容修改后,serialVersionUID会改变,java程序不允许序列化版本号不同的类进行反序列化。
那如果几个月后,对这个类进行了升级,增加了一些内容怎么办?
如果确定这个类确实是之前的那个类,类本身是合法的,可以将序列化版本号写死。
1 | private static final long serialVersionUID = 1231231231231L; |
serial注解
1 | import java.io.Serial |
transient关键字
transient关键字修饰的属性不会参与序列化。
所以进行反序列化的时候这个属性会赋默认值。
打印流 PrintStream/PrintWriter
PrintStream
主要用于打印,提供便携的打印方法和格式化输出。主要打印内容到文件或控制台。
不需要手动刷新。
构造方法
PrintStream(OutputStream out);
PrintStream(String filename);
常用方法
print(Type x);
println(Type x);
PrintWriter
比PrintStream多一个构造方法:PrintWriter(Writer);
标准输入流 System.in
用来接收用户在控制台上的输入。
1 | InputStream in = System.in; |
对于标准输入流来说,也可以改变数据源。不让其从控制台读数据,而是从文件中/网络中读取数据。
1 | // 修改标准输入流的数据源 |
标准输出流 System.out
用于输出内容到控制台。
改变输出方向:(常用于记录日志)
1 | System.setOut(new PrintStream("filename")); |
File类
文件/目录的抽象表示形式。
构造方法
1 | File file = new File("e:/filename"); |
常见方法见文档。
1 | File file = new File("e:/directoryAddress"); |
读取属性配置文件
- xxx.properties 文件称为属性配置文件
- 属性配置文件可以配置一些简单的信息,例如连接数据库的信息通常配置到属性文件中。这样可以做到在不修改java代码的前提下,切换数据库。
- 属性配置文件的格式:
key1 = value1
key2 = value2
…
注:使用#进行注释,key不能重复,否则value会被覆盖。等号两边不能有空格。
1 | String path = Thread.currentThread().getContextClassLoader().getResource("filename").getPath(); |
ResourceBundle进行资源绑定
装饰器设计模式
符合OCP的情况下怎么完成对类功能的扩展?
- 使用子类对父类进行方法扩展。但这种方法会导致两个问题:代码耦合度高、类爆炸问题(会有很多类)
- 装饰器设计模式:可以做到在不修改原有代码的基础上,完成功能扩展,符合OCP原则,并且避免了使用继承带来的类爆炸问题。
装饰器设计模式中涉及的角色:
- 抽象的装饰者
- 具体的装饰者1、具体的装饰者2
- 被装饰者
- 装饰者和被装饰者的公共接口/公共抽象类
IO流中使用了大量的装饰器设计模式。
压缩流
压缩流的使用
1 | public class GZIPOutputStreamTest { |
解压缩流的使用
1 | public class GZIPInputStreamTest { |
注:节点流关闭时会自动刷新,包装流需要手动刷新。
字节数组流
- ByteArrayInputStream、ByteArrayOutputStream都是内存操作流,不需要打开和关闭文件等操作。这些流是非常常用的,可以将它们看作开发中的常用工具,能够方便地读写字节数组、图像数据等内存中的数据。
- 都是节点流。
使用对象流装饰字节数组流
!!为什么要这样做?
你使用字节数组流直接写入、读出可能只能读取普通的字节数组,还需要自己实现一些转换成复杂类型(各种类)的方法,而包装流已经在内部包含了很多将复杂类型序列化的方法,一行代码就可以帮你直接序列化复杂类型然后写入字节流。
对象深克隆
目前为止对象拷贝方式:
- 调用Object的clone方法,默认是浅克隆,需要深克隆的话,就需要重写clone方法
- 可以通过序列化和反序列化完成对象的克隆(深克隆)
1 | public class DeepCloneTest { |
九、多线程
概述
多线程
- 进程:操作系统中的一段程序,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。
- 线程:进程中的一个执行单元。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同操作,提高程序的执行效率。一个进程可以有多个线程。
- 静态变量、实例变量是在堆中的,所以是共享的。
并发
使用单核CPU时,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果。
并行
多核CPU,同一时刻,多条指令在多个CPU上同时执行。(无论微观还是宏观)
并发与并行
- CPU比较繁忙时,如果开启了多个线程,则只能为一个线程分配仅有的CPU资源,多线程会竞争CPU资源。
- 在CPU资源比较充足时,一个进程内的多个线程可以被分配到不同的CPU资源,实现并行。
多线程实现的是并发还是并行?如上所述,看运行时CPU的资源,都有可能。
线程的调度模型
多个线程抢夺一个CPU内核的执行权,需要线程调度策略。
分时调度模型
所有线程轮流使用CPU的执行权,并且平均分配每个线程占用的CPU时间
抢占式调度模型
让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就随机选择一个线程获得CPU的执行权。
JAVA采用的就是抢占式调度。
实现多线程的方法
第一种
- 编写一个类继承java.lang.Thread
- 重写run方法
- new线程对象
- 调用线程对象的start()方法来启动线程
1 | public class ThreadTest{ |
start方法的任务是启动一个新线程,分配一个新的栈空间就结束了。
java永远满足一个语法规则:必须自上而下依次逐行运行。
第二种
- 编写一个类实现java.lang.Runnable接口
- 实现接口中的run方法 (此处不能thorws异常)
- new线程对象(把实现Runnable接口的类传给Thread构造方法)
- 调用线程对象的start()方法来启动线程
这种方式更好,因为以后还可以继承别的类。而第一种已经使用掉继承一个类的名额了。
1 | public class ThreadTest{ |
这种方式还可以使用匿名内部类:
1.
1 | public class ThreadTest{ |
2.
1 | public class ThreadTest{ |
线程常用的三个方法
- String getName(); 获取线程对象的名字
- void setName(String threadName); 修改线程的名字
- static Thread currentThread(); 获取当前线程对象的引用
除了使用setName修改线程的名字,还可以使用有参构造方法。但是需要在类中实现这个有参构造方法。
1 | public MyThread(String threadName){ |
线程生命周期的7个状态
- 新建状态 NEW
- 就绪状态
- 运行状态 (2-3 官方统称为可运行状态RUNNABLE)
- 超时等待状态 TIMED_WAITING
- 等待状态 WAITING
- 阻塞状态 BLOCKED
- 终止状态 TERMINATED
线程的休眠
Thread.sleep(毫秒数);
在规定的时间内,当前线程没有权利抢夺CPU时间片了。
中断线程的休眠
interrupt()是一个实例方法。
线程对象.interrupt();
可以中断线程的休眠。(当然要放在另一个线程里使用才能起作用)底层原理是利用了异常处理机制。
当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterrupttedException,然后捕捉异常,终止睡眠。
停止运行线程
线程对象.stop() 已经不建议使用
一般是设置一个标记,然后在线程的循环中使用if语句判断这个标记。
比如 boolean run = true; 当达到某个条件后将run改为false,然后if(run){ 运行的内容 } else{return;}
return后就终止这个线程了。
守护线程
在java中,线程被分为两类:守护线程、用户线程
所有用户线程结束后,守护线程自动退出/结束。
在JVM中,有一个隐藏的守护线程一直在守护着,它就是GC线程。
将线程设置为守护线程:
线程对象.setDaemon(true);
定时任务
java.util.Timer 定时器
java.util.TimerTask 定时任务
1 | // 创建定时器对象(本质上就是一个线程) |
线程合并
join() 方法是一个实例方法
t.join()
是让当前线程进入阻塞状态,直到t线程结束,当前线程的阻塞状态结束。
个人理解:就是先让t线程打断当前线程自己运行,如果设置的时间结束或者t线程在时间结束前已经运行完了,那当前线程就继续执行。
线程优先级
最低1(Thread.MIN_PRIORITY
),最高10(Thread.MAX_PRIORITY
)
t.setPriority(传入优先级数值)
让位
静态方法:Thread.yield()
让当前线程让位。让位不会让其进入阻塞状态,只是放弃当前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
线程安全问题
什么情况下需要考虑线程安全问题?
- 多线程并发
- 有共享的数据
- 共享数据涉及修改操作
一般情况下
局部变量不存在线程安全问题。(尤其是基本数据类型,但如果是引用数据类型就另说了。)
实例变量、静态变量可能存在线程安全问题。他们存放在堆中,堆是多线程共享的。
线程同步机制——互斥锁
线程排队执行
现有t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。
1 | synchronized(obj){ // obj为共享对象,在银行取款的例子中,这个共享对象就是账户 |
假设t1先抢到了CPU时间片,t1线程找到共享对象obj的对象锁后占有这把锁,t2只能在同步代码块之外等待,等t1线程执行完同步代码块之后,才会释放之前占有的对象锁。
synchronized又被称为互斥锁。
synchronized也可以作为标识符直接写在方法(实例方法、静态方法)声明上,
静态方法检测的是类锁,实例方法检测的是对象锁。
线程异步机制
线程并发执行
各自执行各自的,谁也不需要等对方。
效率高但可能存在安全隐患。
线程通信
涉及到的三个方法:
wait()
、notify()
、notifyAll()
- 以上三个方法都是Object类的方法。
- 调用wait方法和notify方法是通过共享对象去调用的。
例如:obj.wait()的效果:在obj对象上活跃的所有线程进入无期限等待,直到调用了该共享对象的notify方法进行唤醒,唤醒后会接着上一次调用wait方法的位置继续执行。
- obj.wait() 调用后会释放之前占用的对象锁。
- obj.notify() 唤醒优先级最高的等待线程,如果优先级一样,就随机唤醒一个。
- obj.notifyAll() 唤醒所有在该共享对象上等待的线程
最完整的生命周期
懒汉式单例模式的线程安全问题
1 | class SingletonTest { |
Lock 和 synchronized 哪个好?
Lock更好,因为更加灵活。synchronized代码块的大括号必须包住所有语句,而unlock()可以任意插入到一些语句中,但一定要记得执行unlock()
创建线程的第三种方法——未来任务
优点:可以拿到线程执行结束的返回值
1 | // 创建“未来任务”对象 |
创建线程的第四种方式——线程池
服务器启动时,创建N个线程对象,直接放到线程池中,需要的时候把任务交给线程池即可。
1 | // 创建一个线程池对象(线程池中有3个线程) |
十、反射 reflect
概述
- 后续学的大量java框架都是基于反射机制实现的。
- 反射机制可以让程序更加灵活
- 反射机制最核心的几个类:
java.lang.Class : Class类型的实例代表硬盘上某个class文件,或者说代表某一种类型
java.lang.reflect.Filed : 实例代表类中的属性/字段
java.lang.reflect.Constructor : 它的实例代表类中的构造方法
java.lang.reflect.Method : 它的实例代表类中的方法
获取Class的四种方式
第一种
Class c = Class.forName("完整的全限定类名");
注:
- 全限定类名是带有包名的,不可省略
- 这是个字符串参数
- 如果这个类根本不存在,会报异常:
java.lang.ClassNotFoundException
- 这个方法的执行会导致类的加载动作的发生
第二种
Class c = obj.getClass();
第三种
Class c = 类名.class;
第四种——使用类加载器
1 | ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); |
Class.forName()
和 classLoader.loadClass()
的区别:
Class.forName()
:类加载时会进行初始化(静态变量赋值、静态代码块执行)。
classLoader.loadClass()
:类加载时不会进行初始化,直到第一次使用该类。
通过反射机制实例化对象
直接对类使用newInstance方法
1 | Class userClass = Class.forName("test.User"); |
使用反射机制,只要修改属性配置文件就可以完成不同对象的实例化。非常灵活
1 | ResourceBundle bundle = ResourceBundle.getBundle("test.classInfo"); |
使用这种方式必须要有一个无参数构造方法。如果没有会出现异常。
Java9时被标注已过时,不建议使用。
使用构造方法实例化对象
无参构造:
1 | Class userClass = Class.forName("test.User"); |
有参构造:
1 | Class userClass = Class.forName("test.User"); |
通过反射为对象属性赋值
1 | Class clazz = Class.forName("com.powernode.javase.reflect.Customer"); |
反射某一个类的方法
类加载的过程
虚拟机的三个类加载器
1 | // 通过自定义的类获取的类加载器是:应用类加载器。 |
双亲委派机制
- 某个类加载器接收到加载类的任务时,通常委托给“父 类加载器”进行加载
- 最大的“父 类加载器”无法加载时,一级一级向下委托加载任务
作用:
- 保护程序的安全
- 防止类加载重复
获取泛型
获取父类的泛型
1 | // 获取类 |
获取接口的泛型
1 | public class Test { |
获取属性上的泛型
1 | public class User { |
获取方法参数、返回值上的泛型
1 | public class MyClass { |
获取构造方法函数上的泛型
1 | public class User { |
十一、注解
概述
什么是注解?
- JDK1.5引入
- 可以标注在类上、方法上、属性上、构造方法上、方法参数上等……
- 注解可以做到在不改变代码逻辑的前提下在代码中嵌入补充信息
注解与注释
注解:给编译器或者其他程序看的,程序根据注解来决定不同的处理方式
注释:给程序员看
框架
框架 = 反射 + 注解 + 设计模式
内置的注解
@Deprecated
用来标记过时的元素,在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素比如过时的类、过时的方法、过时的属性等。
@Override
修饰实例方法,则该方法必须是个重写方法,否则就会编译失败。
@SuppressWarnings(抑制警告的注解)
在实际开发中,建议尽量不要忽略警告,而是真正的去解决警告。
@SuppressWarnings(“rawtypes”):抑制未使用泛型的警告
@SuppressWarnings(“resource”):抑制未关闭资源的警告
@SuppressWarnings(“deprecation”):抑制使用了已过时资源时的警告
@SuppressWarnings(“all”):抑制所有警告
@FunctionalInterface
“函数式接口”的注解,这个是 JDK1.8 版本引入的新特性。使用@FunctionalInterface标注的接口,则该接口就有且只能存在一个抽象方法,否则就会发生编译错误。
(注意:接口中的默认方法或静态方法可以有多个。)
自定义注解
如何自定义
创建一个annotation类型的文件,在文件中写入注解的定义。
1 | public MyAnnotation { |
注解中定义属性
属性的类型只能是:
- byte,short,int,long,float,double,boolean,char
- String、Class、枚举类型、注解类型
- 以上所有类型的一维数组形式
1 | public MyAnnotation { |
- 如果属性只有一个,并且属性名是value,那使用注解的时候可以省略value这个属性名。
1 | public MyAnnotation { |
- 如果属性是一个数组,使用注解时,数组值只有一个,数组的大括号可以省略。
1 | public MyAnnotation { |
元注解
@Retention:设置注解的保持性
注解存在阶段是保留在源代码(编译期),字节码(类加载)或者运行时(JVM中运行)
@Retention(RetentionPolicy.SOURCE)
:注解仅存在于源代码中,在字节码文件中不包含。@Retention(RetentionPolicy.CLASS)
:注解在字节码文件中存在,但运行时无法获得(默认)。@Retention(RetentionPolicy.RUNTIME)
:注解在字节码文件中存在,且运行时可通过反射获取。
@Target:设置注解可以使用的位置
@Target(ElementType.TYPE)
:作用于接口、类、枚举、注解@Target(ElementType.FIELD)
:作用于属性、枚举的常量@Target(ElementType.METHOD)
:作用于方法@Target(ElementType.PARAMETER)
:作用于方法参数@Target(ElementType.CONSTRUCTOR)
:作用于构造方法@Target(ElementType.LOCAL_VARIABLE)
:作用于局部变量@Target(ElementType.ANNOTATION_TYPE)
:作用于注解@Target(ElementType.PACKAGE)
:作用于包@Target(ElementType.TYPE_PARAMETER)
:作用于泛型,即泛型方法、泛型类和泛型接口。@Target(ElementType.TYPE_USE)
:作用于任意类型。
@Documented:设置注解会被包含在API文档中
使用javadoc.exe工具可以从程序源代码中抽取类、方法、属性等注释形成一个源代码配套的API帮助文档,而该工具抽取时默认不包括注解内容。如果注解被@Documented标注,那么就能被javadoc.exe工具提取到API文档。
@Inherited:被标注的注解支持继承
使用后子类会继承父类的注解。
@Repeatable:设置后可以在一个地方重复使用同一注解(java8)
@Repeatable(原注解的复数形式)
但是需要再声明一个原来注解的复数形式,并在其中包含原注解类型的数组。
1 | public class Test { |
1 |
|
1 | public Authors { |
反射注解
获取类上的所有注解
Annotation[] annotations = clazz.getAnnotations();
获取类上指定的某个注解
clazz.isAnnotationPresent(AnnotationTest01.class)
AnnotationTest01 an = clazz.getAnnotation(AnnotationTest01.class);
获取属性上的所有注解
Annotation[] annotations = field.getAnnotations();
获取属性上指定的某个注解
field.isAnnotationPresent(AnnotationTest02.class)
AnnotationTest02 an = field.getAnnotation(AnnotationTest02.class);
获取方法上的所有注解
Annotation[] annotations = method.getAnnotations();
获取方法上指定的某个注解
method.isAnnotationPresent(AnnotationTest02.class)
AnnotationTest02 an = method.getAnnotation(AnnotationTest02.class);
十二、网络编程
概述
网络编程的三个基本要素:
- IP地址:定位网络中的某台计算机
- 端口号port:定位计算机上的某个进程(某个应用)
- 通信协议:通过IP地址和端口号定位后,如何保证数据可靠高效的传输,就需要依靠通信协议。
IP地址
- IPv4:4字节,xxx.xxx.xxx.xxx 每个xxx表示8位二进制数,范围是0-255
前三个字节用于表示网络(省市区),最后一个字节用于表示主机(家门牌号)
一些IP地址被保留或者被私有机构使用,不能用于公网的地址分配;还有一些IP地址被用作多播地址,仅用于特定的应用场景。因此实际可以使用的IPv4地址少于总量。
IPv6:16字节,由8组十六进制数表示,如 3ffe:3201:1401:1280:c8ff:fd54:db39:1984
本机地址:127.0.0.1,主机名:localhost
192.168.0.0-192.168.255.255为私有地址,属于非注册地址,专门为组织、机构内部使用。(用于局域网)
端口号port
用两个字节(无符号)表示的,取值范围0-65535,计算机端口号可以分为三大类:
- 公认端口:0-1023,被预先定义的服务通信占用(如http占用80,FTP占用21,Telnet占用23等)
- 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
- 动态/私有端口:49152~65535。
通常情况下,服务器程序使用固定的端口号来监听客户端的请求,而客户端则使用随机端口连接服务器。
OSI参考模型
TCP/IP参考模型
网络编程基础类
InetAddress类
- java.net.IntAddress类用来封装计算机的IP地址和DNS(没有端口信息),它包括一个主机名和一个IP地址,是java对IP地址的高层表示。大多数其它网络类都要用到这个类,包括Socket、ServerSocket、URL、DatagramSocket、DatagramPacket等
- 常用静态方法
static InetAddress getLocalHost()
得到本机的InetAddress对象,其中封装了IP地址和主机名lstatic InetAddress getByName(String host)
传入目标主机的名字或IP地址得到对应的InetAddress对象,其中封装了IP地址和主机名(底层会自动连接DNS服务器进行域名解析)
- 常用实例方法
lpublic String getHostAddress()
获取IP地址lpublic String getHostName()
获取主机名/域名
URL类
- URL是统一资源定位符,是互联网上资源位置和访问方法的一种简介表示。每个文件具有唯一的URL。
- URL由4部分组成:协议、存放资源的主机域名、端口号、资源文件名。如果未指定端口号,则使用协议默认的端口。HTTP协议的默认端口为80。
- URL的标准格式:<协议>://<域名或IP>:<端口>/<路径>,其中端口和路径有时可以省略。
- 为了方便程序员编程,JDK提供了java.net.URL类,该类封装了大量复杂的涉及从远程站点获取信息的细节,可以使用它的各种方法对URL对象进行分割、合并等处理
构造方法
1 | URL url = new URL("http://127.0.0.1:8080/oa/index.html?name=zhangsan#tip"); |
常用方法
获取协议:url.getProtocol()
获取域名:url.getHost()
获取默认端口:url.getDefaultPort()
获取端口:url.getPort()
获取路径:url.getPath()
获取资源:url.getFile()
获取数据:url.getQuery()
获取锚点:url.getRef()
openStream()
:可以打开到此URL的连接并返回一个用于从该连接读入的InputStream,实现最简单的爬虫。
TCP 与 UDP 协议
Socket 套接字
Socket是传输层供给应用层的编程接口。使用Socket编程可以开发客户端和服务器应用程序,可以在本地网络上进行通信,也可以通过互联网在全球范围内通信。
TCP协议和UDP协议是传输层的两种协议。Socket编程分为TCP编程和UDP编程两类。
TCP、UDP协议
TCP 三次握手(通道打开)
- 客户端发送SYN(同步)数据包,包含客户端的初始序列号(ISN)
- 服务器收到SYN数据包后,发送SYN-ACK(同步确认)数据包,包含服务器的初始序列号(ISN)和对客户端ISN的确认号(ACK)
- 客户端收到SYN-ACK数据包后,发送ACK(确认)数据包,包含对服务器ISN的确认号(ACK)
三次握手完成后,客户端和服务器就可以开始交换数据了。
三次握手的意义:不会丢失、重复、乱序,保证数据在两个设备之间可靠地传输。
四次挥手(通道关闭)
- 客户端发送FIN(结束)数据包,表示客户端已经完成数据传输,希望关闭连接。
- 服务器收到FIN数据包后,发送ACK(确认)数据包,表示服务器已经收到客户端的FIN数据包,同意关闭连接。
- 服务器发送FIN数据包,表示服务器已经完成数据传输,希望关闭连接。
- 客户端收到FIN数据包,发送ACK(确认)数据包。表示客户端已经收到服务器的FIN数据包,并同意关闭连接。
四次挥手完成后,客户端和服务器之间的连接就关闭了。
四次挥手的意义:不会丢失、重复、乱序,保证数据在两个设备之间可靠地传输。
基于TCP协议的编程
概述
- 在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client),而在第一次通讯中等待连接的程序被称作服务端(Server)。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
- 套接字与主机地址和端口号相关联,主机地址就是客户端或服务器程序所在的主机的IP地址,端口地址是指客户端或服务器程序使用的主机的通信端口。在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样客户端和服务器通过套接字所建立连接并使用IO流进行通信。
Socket类
Socket实现客户端套接字。
构造方法:
public Socket(InetAddress a, int p)
创建套接字并连接到指定IP地址的指定端口号
Socket类实例方法:
public InetAddress getInetAddress()
返回此套接字连接到的远程 IP 地址
public InputStream getInputStream()
返回此套接字的输入流(接收网络消息)
public OutputStream getOutputStream()
返回此套接字的输出流(发送网络消息)
public void shutdownInput()
禁用此套接字的输入流
public void shutdownOutput()
禁用此套接字的输出流
public synchronized void close()
关闭此套接字(默认会关闭IO流)
ServerSocket类
ServerSocket类实现服务器套接字。服务器套接字等待请求通过网络传入,基于该请求执行某些操作,然后向请求者返回结果。
构造方法:
public ServerSocket(int port)
ServerSocket类实例方法:
public Socket accept()
侦听要连接到此套接字并接受它
public InetAddress getInetAddress()
返回此服务器套接字的本地地址
public void close()
关闭此套接字
十三、lambda表达式
- 面向对象的思想
- 只做一件事情,找一个能解决这个事情的对象,然后调用对象的方法完成这件事情。
- 函数式编程思想
- 只要能获得结果,谁去做的,怎么做的都不重要,重视结果,忽略实现过程
Lambda和匿名内部类的区别
- 所需类型不同
- 匿名内部类:可以是接口、抽象类、具体类
- Lambda表达式:只能是接口
- 使用限制不同
- 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。
- 如果接口中有多个抽象方法,就只能使用匿名内部类,而不能使用Lambda表达式。
- 实现原理不同
- 匿名内部类:编译之后,会生成一个单独的.class字节码文件
- Lambda表达式:编译之后,不会生成一个单独的.class字节码文件
Lambda表达式的语法
1 | (形参列表) -> { |
例:
1 | List<Integer> list = Arrays.asList(100,200,350,300); |
Lambda 表达式的语法精简
四种情况:
- 形参类型可以省略,如果需要省略,则每个形参的类型都要省略。
- 如果形参列表只有一个形参,那么形参类型和小括号都可以省略。
- 如果方法体重只有一行语句,那么方法体的大括号也可以省略。
- 如果方法体中只有一条return语句,那么大括号可以省略,且必须去掉return关键字。
四个基本的函数式接口
名字 | 接口名 | 对应的抽象方法 |
---|---|---|
消费 | Consumer |
void accept(T t); |
生产 | Supplier |
T get(); |
转换 | Function<T, R> | R apply(T t); |
判断 | Predicate |
boolean test(T t); |
Lambda表达式的方法引用(简化Lambda表达式)
方法引用的概述
我们在使用Lambda表达式的时候,如果Lambda表达式的方法体中除了调用现有方法之外什么都不做,满足这样的条件就有机会使用方法引用来实现。
在以下的代码中,在重写的apply()方法中仅仅只调用了现有Math类round()方法,也就意味着Lambda表达式中仅仅只调用了现有Math类round()方法,那么该Lambda表达式就可以升级为方法引用,案例如下:
1 | // 需求:实现小数取整的操作 |
对于方法引用,我们可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
在Lambda表达式的方法引用中,主要有实例方法引用、静态方法引用、特殊方法引用和构造方法引用、数组引用这五种情况,接下来我们就对这五种情况进行讲解。
实例方法引用
语法:对象 :: 实例方法
特点:在Lambda表达式的方法体中,通过“对象”来调用指定的某个“实例方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过对象调用某个实例方法的返回值类型和形参列表 保持一致。
【示例】实例化Consumer接口的实现类对象,并在重写的accept()方法中输出形参的值
1 | // 方式一:使用匿名内部类来实现 |
【示例】实例化Supplier接口的实现类对象,并在重写方法中返回Teacher对象的姓名
1 | Teacher teacher = new Teacher("ande", 18); |
静态方法引用
语法:类 :: 静态方法
特点:在Lambda表达式的方法体中,通过“类名”来调用指定的某个“静态方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过类名调用某个静态方法的返回值类型和形参列表保持一致。
【示例】实例化Function接口的实现类对象,并在重写的方法中返回小数取整的结果
1 | // 方式一:使用匿名内部类来实现 |
特殊方法引用
语法:类名 :: 实例方法
特点:在Lambda表达式的方法体中,通过方法的第一个形参来调用指定的某个“实例方法”。
要求:把函数式接口中抽象方法的第一个形参作为方法的调用者对象,并且从第二个形参开始(或无参)可以对应到被调用实例方法的参数列表中,并且返回值类型保持一致。
【示例】使用Comparator比较器,来判断两个小数的大小
1 | // 方式一:使用匿名内部类来实现 |
需求:实例化Function接口的实现类对象,然后获得传入Teacher对象的姓名。
1 | // 方式一:使用匿名内部类来实现 |
构造方法引用
语法:类名 :: new
特点:在Lambda表达式的方法体中,返回指定“类名”来创建出来的对象。
要求:创建对象所调用构造方法形参列表 和 函数式接口中的方法的形参列表 保持一致,并且方法的返回值类型和创建对象的类型保持一致。
【示例】实例化Supplier接口的实现类对象,然后调用重写方法返回Teacher对象
1 | // 方式一:使用匿名内部类来实现 |
【示例】实例化Function接口的实现类对象,然后调用重写方法返回Teacher对象
1 | // 方式一:使用匿名内部类来实现 |
数组引用
语法:数组类型 :: new
特点:在Lambda表达式的方法体中,创建并返回指定类型的“数组”。
要求:重写的方法有且只有一个整数型的参数,并且该参数就是用于设置数组的空间长度,并且重写方法的返回值类型和创建数组的类型保持一致。
【示例】实例化Function接口的实现类对象,并在重写方法中返回指定长度的int类型数组
1 | // 方式一:使用匿名内部类来实 |
Lambda在集合当中的使用
为了能够让Lambda和Java的集合类集更好的一起使用,集合当中也新增了部分方法,以便与Lambda表达式对接,要用Lambda操作集合就一定要看懂源码。
forEach()方法
在Collection集合和Map集合中,都提供了forEach()方法用于遍历集合。
在Collection集合中,提供的forEach()方法的形参为Consumer接口(消费型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】遍历List集合中的元素
1 | List<Integer> list = Arrays.asList(11, 22, 33, 44, 55); |
【示例】遍历Set集合中的元素
1 | List<String> list = Arrays.asList("aa", "bb", "cc", "dd"); |
在Map集合中,提供的forEach()方法的形参为BiConsumer接口,而BiConsumer接口属于两个参数的消费型接口,通过该方法再配合Lambda表达式就可以遍历Map集合中的元素。
【示例】遍历Map集合中的元素
1 | // 实例化Map集合并添加键值对 |
removeIf()方法
在Collection集合中,提供的removeIf()方法的形参为Predicate接口(判断型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】删除List集合中的某个元素
1 | // 创建List集合并添加元素 |
【示例】删除Set集合中的某个元素
1 | List<String> list = Arrays.asList("aa", "bb", "cc", "dd"); |