Java基础知识(二)(Object类的常用方法、日期时间类、System类、StringBuilder类、包装类、Collection集合、Iterator迭代器、泛型、list集Set接口...)

文章目录

  • Java基础知识(二)
    • 1.Object类的常用方法
      • 1.1 toString方法
      • 1.2 equals方法
      • 1.3 Objects类
    • 2.日期时间类
      • 2.1 Date类
      • 2.2 DateFormat类
      • 2.3 Calendar类
    • 3.System类
    • 4.StringBuilder类
    • 5.包装类
      • 5.1 装箱与拆箱
        • 装箱
        • 拆箱
      • 5.2 自动装箱与自动拆箱
      • 5.3 基本类型与字符串之间的转换
    • 6.Collection集合
      • 6.1 Collection 常用功能
    • 7.Iterator迭代器
      • 7.1 Iterator接口的常用方法如下:
      • 7.2 增强for
    • 8.泛型
      • 8.1 泛型的定义与使用
      • 8.2 泛型通配符
    • 9.list集合
    • 10.List的子类
      • 10.1 ArrayList集合
      • 10.2 LinkedList集合
    • 11.Set接口
      • 11.1 Set集合存储元素不重复的原理
      • 11.2 HashSet集合介绍
      • 11.3 HashSet集合存储数据的结构(哈希表)
      • 11.4 HashSet存储自定义类型元素
      • 11.5 LinkedHashSet
      • 11.6 可变参数
    • 12.Collections常用功能
    • 13.Map
      • 13.1 Map常用实现类
      • 13.2 Map接口中的常用方法
      • 13.3 Map集合遍历键找值方式
      • 13.4 HashMap存储自定义类型键值
      • 13.5 LinkedHashMap类
      • 13.6 Hashtable集合
      • 13.7 JDK9对集合添加的优化
    • 14.异常
      • 14.1 异常体系
      • 14.2 异常分类
      • 14.3 异常的产生过程解析
      • 14.4 异常的处理
        • 14.4.1 抛出异常throw
        • 14.4.2 Objects非空判断
        • 14.4.3 声明异常throws
        • 14.4.4 捕获异常try…catch
        • 14.4.5 finally 代码块
        • 14.4.6 异常注意事项
        • 14.4.7 自定义异常类
    • 15.多线程
      • 15.1 并发与并行
      • 15.2 线程与进程
      • 15.3 创建线程类
      • 15.4 Thread类
      • 15.5 Runnable接口
      • 15.6 匿名内部类实现线程的创建
    • 16.线程安全
      • 16.1 线程安全
      • 16.2 线程同步
        • 16.2.1 同步代码块
        • 16.2.2 同步方法
        • 16.2.3 Lock锁
    • 17.线程的状态
      • 17.1 Timed Waiting
      • 17.2 Blocked
      • 17.3 Waiting
    • 18.等待唤醒机制
      • 18.1 线程间通信
      • 18.2 等待唤醒机制
    • 19.线程池
      • 19.1 线程池概念
      • 19.2 线程池的使用
    • 20.Lambda表达式
      • 20.1 函数式编程思想概述
      • 20.2 冗余的Runnable代码->体验Lambda的更优写法
      • 20.3 Lambda标准格式
      • 20.4 Lambda省略格式&Lambda的使用前提
    • 21.File类
      • 21.1 常用方法
        • 21.1.1 获取功能的方法
        • 21.1.2 判断功能的方法
        • 21.1.3 创建/删除功能的方法
      • 21.2 目录的遍历
      • 21.3 文件过滤器
    • 22.递归
    • 23.IO概述
    • 24.字节流
      • 24.1 字节输出流【OutputStream】
        • 24.1.1 FileOutputStream类
      • 24.2 字节输入流【InputStream】
        • 24.2.1 FileInputStream类
    • 25.字符流
      • 25.1 字符输入流【Reader】
        • 25.1.1 FileReader类
      • 25.2 字符输出流【Writer】
        • 25.2.1 FileWriter类
    • 26.IO异常的处理
    • 27.属性集
      • 27.1 Properties类
    • 28.缓冲流
      • 28.1 字节缓冲流
      • 28.2 字符缓冲流
    • 29.转换流
      • 29.1 字符编码和字符集
      • 29.2 InputStreamReader类
      • 29.3 OutputStreamWriter类
    • 30.序列化
      • 30.1 ObjectOutputStream类
      • 30.2ObjectInputStream类
    • 31.打印流
      • 31.1 PrintStream类
    • 32.网络编程入门
      • 32.1 软件结构
      • 32.2 网络通信协议
      • 32.3 协议分类
      • 32.4 网络编程三要素
    • 33.TCP通信程序
      • 33.1 Socket类
      • 33.2 ServerSocket类
      • 33.3 简单的TCP网络程序
      • 33.4 TCP文件上传案例
    • 34.函数式接口
      • 34.1 格式
      • 34.2 @FunctionalInterface注解
      • 34.3 自定义函数式接口
    • 35.函数式编程
      • 35.1 Lambda的延迟执行
      • 35.2 使用Lambda作为参数和返回值
    • 36.常用函数式接口
      • 36.1 Supplier接口
      • 36.2 Consumer接口
      • 36.3 Predicate接口
      • 36.4 Function接口
    • 37.Stream流
      • 37.1 流式思想概述
      • 37.2 获取流
      • 37.3 常用方法
    • 38.方法的引用
      • 38.1 方法引用符
      • 38.2 通过对象名引用成员方法
      • 38.3 通过类名称引用静态方法
      • 38.4 通过super引用成员方法
      • 38.5 通过this引用成员方法
      • 38.6 类的构造器引用
      • 38.7 数组的构造器引用

Java基础知识(二)

1.Object类的常用方法

java.lang.Object类是Java语言中的根类,所有类的父类。
Object描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object。
如果一个类没有特别指定父类,那么默认则继承自Object类。

1.1 toString方法

public String toString(); 返回该对象的字符串表示。

  • 该字符串内容就是对象的类型+@+内存地址值。由于toString方法返回的结果是内存地址,而在开发中,经常需要按照对象的属性得到相应的字符串表现形式,因此也需要重写它。
  • 在直接使用输出语句输出对象名的时候,其实通过该对象调用了其toString()方法。

1.2 equals方法

public boolean equals(Object obj):指示其他某个对象是否与此对象“相等”

  • 默认地址比较
    如果没有覆盖重写equals方法,那么Object类中默认进行==运算符的对象地址比较,只要不是同一个对象,结果必然为false。
  • 对象内容比较
    如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法。

1.3 Objects类

  • 在JDK7添加了一个Objects类,它提供了一些方法来操作对象,它由一些静态的使用方法组成,这些方法是null-save(空指针安全)或null-tolerant(容忍空指针)的
  • 在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题,方法如下:
    public static boolean equals(Object a, Object b):判断两个对象是否相等
public static boolean equals(Object a,Object b){
    return  (a==b)||(a!=null&&a.equals(b));

}

2.日期时间类

2.1 Date类

java.util.Date类 表示特定的瞬间,精确到毫秒。

  • public Date():分配Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。
  • public Date(long date):分配Date对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即1970年1月1日00:00:00 GMT(英国格林威治))以来的指定毫秒数。中国属于东八区,1970年1月1日08:00:00。

2.2 DateFormat类

  • java.text.DateFormat是日期/时间格式化子类的抽象类,我们可以通过这个类完成日期和文本之间的转换,也就是在Date对象和String对象之间进行转换
  • 格式化: 按照指定的格式,从Date对象转换为String对象
  • 解析:按照指定的格式,从String对象转换为Date对象
  • 由于DateFormat为抽象类,不能直接使用,所以需要常用的子类java.text.SimpleDateFormat;这个类需要一个模式(格式)来指定格式化或解析的标准
  • 构造方法:
    public SimpleDateFormat(String pattern):用给定的模式和默认语言环境的日期格式符号构造
    参数pattern是一个字符串,代表日期时间的自定义格式。
  • 成员方法:
    String format(Date date):按照指定的模式,把Date对象格式化为符合模式的字符串
    Date parse(String source):按照指定的模式,把符合模式的字符串格式化为Date对象
    parse()声明了一个异常 ParseException(解析异常)如果字符串和构造方法的模式不一样,即抛出异常
标记字母 表示含义
y
M
d
H
m
s
  • “yyyy-MM-dd HH:mm:ss” 模式中的字母不能更改,连接模式的符号可以改变 “yyyy年MM月dd日 HH时mm分ss秒”

2.3 Calendar类

  • 该类将所有可能用到的时间信息封装为静态成员变量,方便获取。Calendar为抽象类,在创建对象时并非直接创建,而是通过静态方法创建,返回子类对象
  • public static Calendar getInstance():使用默认时区和语言环境获得一个日历(Calendar子类对象)

成员方法:

  • public int get(int field):返回给定日历字段的值。
  • public void set(int field, int value):将给定的日历字段设置为给定值
  • public Date getTime():返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象
  • public abstract void add(int field, int amount):根据日历的规则,为给定的日历字段添加或减去指定的时间量(int field:日历类的对象,可以使用Calendar的静态成员获取)
字段值 含义
YEAR
MONTH 月(从0开始,可以+1使用)
DAY_OF_MONTH 月中的天(几号)
HOUR 时(12小时制)
HOUR_OF_DAY 时(24小时制)
MINUTE
SECOND
DAY_OF_WEEK 周中的天(周几,周日为1,可以-1使用)

3.System类

  • java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作
  • 静态成员方法:
  • public static long currentTimeMillis():返回以毫秒为单位的当前时间。
    实际就是获取当前系统时间与1970年01月01日00:00点之间的毫秒差值
  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
    src-源数组
    srcPos-源数组中的起始位置(起始索引)
    dest-目标数组
    destPos-目标数组的起始位置
    length-要复制的数组元素的数量

4.StringBuilder类

  • String:字符串是常量,它们的值在创建后不能被更改。底层是一个被final修饰的数组
    private final byte[] value
  • StringBuilder又称为可变字符序列,它是一个类似于 String的字符串缓冲区,即它是一个容器,容器中可以装很多字符串。通过某些方法调用可以改变该序列的长度和内容
  • 它的内部拥有一个数组用来存放字符串内容,进行字符串拼接时,直接在数组中加入新内容。StringBuilder会自动维护数组的扩容(默认16字符空间,超过自动扩充)
  • 构造方法:
    public StringBuilder():构造一个空的StringBuilder容器。
    public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去。
  • 成员方法:
    public StringBuilder append(…):添加任意类型数据的字符串形式,并返回当前对象自身。
    public String toString():将当前StringBuilder对象转换为String对象。

5.包装类

  • Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类

可以使用一个类,把基本数据类型的数据装起来,在类中定义一些方法,整个类叫做包装类,我们可以使用类中的方法来操作这些基本类型的数据(位于java.lang包中)

基本类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

5.1 装箱与拆箱

基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“:

  • 装箱:从基本类型转换为对应的包装类对象。
  • 拆箱:从包装类对象转换为对应的基本类型。

装箱

  • 构造方法:
    Integer(int value): 构造一个新分配的 Integer 对象,它表示指定的 int 值。
    Integer(String s): 构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。
    传递的字符串,必须是基本类型的字符串,否则会抛出异常 “100” 正确 “a” 抛异常
  • 静态方法:
    static Integer valueOf(int i): 返回一个表示指定的 int 值的 Integer 实例。
    static Integer valueOf(String s): 返回保存指定的 String 的值的 Integer 对象。

拆箱

  • 成员方法:
    int intValue() 以int类型返回该 Integer 的值。

5.2 自动装箱与自动拆箱

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。

5.3 基本类型与字符串之间的转换

  • 基本类型->字符串(String)
  • 基本类型的值+” “ ,最简单的方法(工作中常用)
  • 包装类的静态方法,static String toString(int i) 返回一个表示指定整数的 String 对象
  • String类的静态方法,static String valueOf(int i) 返回 int 参数的字符串表示形式
  • 字符串(String)->基本类型
  • 使用包装类的静态方法parseXXX(“字符串”);
    public static byte parseByte(String s):将字符串参数转换为对应的byte基本类型。
    public static short parseShort(String s):将字符串参数转换为对应的short基本类型。
    public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
    public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
    public static float parseFloat(String s):将字符串参数转换为对应的float基本类型。
    public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。
    public static boolean parseBoolean(String s):将字符串参数转换为对应的boolean基本类型。

6.Collection集合

  • 集合:集合是java中提供的一种容器,可以用来存储多个数据。
  • 数组的长度是固定的。集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象,而且对象的类型 可以不一致,在开发中一般对象多的时候,使用集合进行存储。
  • 集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection双列集合java.util.Map
  • Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素。有两个重要的子接口,分别是java.util.List和java.util.Set。
  • List的特点是元素有序、可重复。其主要实现类有java.util.ArrayList、java.util.LinkedList,java.util.Vector;
  • Set的特点是元素无序、不可重复。其主要实现类有java.util.HashSet和java.util.TreeSet、LinkedHashSet(有序,HashSet子类)。

6.1 Collection 常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。

方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

7.Iterator迭代器

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator。Iterator接口也是Java集合中的一员,但它与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于**迭代访问(即遍历)**Collection中的元素,因此Iterator对象也被称为迭代器。

  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直到把集合中的所有元素全部取出。
  • 迭代:重复、改进、认知升级

7.1 Iterator接口的常用方法如下:

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回true。
  • Iterator迭代器是一个接口,无法直接使用,需要使用Iterator接口的实现类对象,Collection接口有一个方法,叫iterator(),这个方法可以返回一个迭代器的实现类对象,使用Iterator接口接收(多态)
  • Iterator 接口是有泛型的,迭代器的泛型和集合一样,集合是什么泛型,迭代器就是什么泛型

7.2 增强for

增强for循环(for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。

for(元素的数据类型  变量名 : Collection集合/数组){
      //...
}

新for循环必须有被遍历的目标。目标只能是Collection或者是数组。新式for仅仅作为遍历操作出现。

8.泛型

  • 泛型:可以在类或方法中预支地使用未知的类型。
  • 一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

8.1 泛型的定义与使用

  • 定义和使用含有泛型的类
修饰符 class 类名<代表泛型的变量> {
    //...
    }

使用泛型:在创建对象的时候确定泛型

  • 定义和使用含有泛型的方法
修饰符 <代表泛型的变量> 返回值类型 方法名(参数列表){  
        //...
}

使用泛型:调用方法时,确定泛型的类型

  • 定义含有泛型的接口
修饰符 interface 接口名{
    //...
}

定义类时确定泛型的类型
始终不确定泛型的类型,直到创建对象时,确定泛型的类型

8.2 泛型通配符

  • 当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
  • 通配符基本使用 泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用表示未知通配符。
  • 泛型不存在继承关系 Collection list = new ArrayList();这种是错误的。
  • 泛型的上限:
    格式: 类型名称 对象名称
    意义: 只能接收该类型及其子类
  • 泛型的下限:
    格式: 类型名称 对象名称
    意义: 只能接收该类型及其父类型

9.list集合

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。List集合的元素是有序的,即元素的存入顺序和取出顺序一致。
有序可重复、有索引

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index):返回集合中指定位置的元素。
  • public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回更新前的元素。

10.List的子类

10.1 ArrayList集合

java.util.ArrayList集合数据存储的结构是数组。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。

10.2 LinkedList集合

java.util.LinkedList集合数据存储的结构是双向链表。方便元素的添加和删除
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public void push(E e):将元素推入此列表所表示的堆栈(first).
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public E pop():从此列表所表示的堆栈处弹出一个元素(first)。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public boolean isEmpty():如果列表不包含元素,则返回true。

11.Set接口

java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与 Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中的元素无序,并且都会以某种规则保证存入的元素不出现重复。
无序不可重复,无索引

11.1 Set集合存储元素不重复的原理

  • hashCode与equals方法

11.2 HashSet集合介绍

  • java.util.HashSet是Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的。java.util.HashSet底层的实现其实是一个java.util.HashMap支持

  • HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素的唯一性依赖于:hashCode与equals方法。

11.3 HashSet集合存储数据的结构(哈希表)

  • 哈希值:逻辑地址
  • Object类方法public native int hashCode():返回该对象的哈希码值
    native:代表该方法调用的本地操作系统的方法

哈希表

  • jdk1.8版本之前:数组+链表
  • jdk1.8版本之后:数组+链表+红黑树(提高查询的速度)
  • 数据结构:相同哈希值的元素分为同一组,链表/红黑树把同一组的元素链接到一起
  • 哈希冲突:不同的元素有相同的哈希值
  • 链表长度超过八位–>转换为红黑树

11.4 HashSet存储自定义类型元素

  • 在HashSet中存放自定义类型元素时,需要重写hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一

11.5 LinkedHashSet

  • HashSet下面有一个子类java.util.LinkedHashSet,它是链表(记录元素的存储顺序)和哈希表组合的一个数据存储结构。保证元素唯一有序

11.6 可变参数

jdk1.5之后出现的新特性

  • 使用条件:当方法的参数列表数据类型已经确定,但是参数的个数不确定,就可以使用可变参数
  • 使用格式:修饰符 返回值类型 方法名 (数据类型…变量名)
  • 可变参数的原理:
    可变参数的底层就是一个数组,根据传递参数的个数不同,会创建不同长度的数组,来存储这些参数
    传递的参数个数,可以是0个(空参)至多个

注意事项:
1.一个方法的参数列表,只能有一个可变参数
2.如果方法的参数有多个,那么可变参数必须写在参数列表的末尾

  • 可变参数的特殊写法: 修饰符 返回值类型 方法名 (Object…变量名)

12.Collections常用功能

java.utils.Collections是集合工具类,用来对集合进行操作。部分方法如下:

  • public static void shuffle(List list):打乱集合顺序。
  • public static void sort(List list):将集合中元素按照默认规则排序。
  • public static void sort(List list,Comparator):将集合中元素按照指定规则排序。
  • public static boolean addAll(Collection c, T… elements):往集合中添加一些元素。
  • Comparable:强行对实现它的每个类对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
  • 被排序的集合的元素,必须实现Comparable重写compareTo()方法定义排序的规则
  • Comparator:强行对某个对象进行整体排序。可以将Comparator传递给sort方法(如Collections.sort或
    Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
  • Comparable接口的排序规则:
    this-参数:升序
    参数-this:降序
  • Comparable和Comparator
  • Comparable:this和参数比较,需要实现Comparable接口,重写比较规则compareTo方法
  • Comparator:相当于一个第三方裁判进行比较,有一个compare方法

13.Map

Java提供了专门的集合类用来存放映射对象关系的对象,即 java.util.Map接口。

  • Collection中的集合,元素是孤立存在的,向集合中存储元素采用一个个元素的方式存储。
  • Map中的集合,元素是成对存在的。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  • Collection中的集合称为单列集合,Map中的集合称为双列集合。
  • 需要注意的是,Map集合不能包含重复的键,值可以重复;每个键只能对应一个值。

13.1 Map常用实现类

  • HashMap:存储数据采用的哈希表结构,无序。由于要保证键的唯一、不重复,需要重写键的
    hashCode()方法、equals()方法。
  • LinkedHashMap:HashMap下的子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证有序;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
  • tips:Map接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。

13.2 Map接口中的常用方法

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定键所对应的键值对元素在Map集合中删除,返回被删除元素的值。
  • public V get(Object key) :根据指定的键,在Map集合中获取对应的值。
  • boolean containsKey(Object key): 判断集合中是否包含指定的键。
  • public Set keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)

13.3 Map集合遍历键找值方式

  • keyset():获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。
  • 遍历键的Set集合,得到每一个键。
  • get(K key):根据键,获取键所对应的值。
  • Entry键值对对象
    Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对对象中获取对应的键与对应的值。
  • public K getKey():获取Entry对象中的键。
  • public V getValue():获取Entry对象中的值。
  • Map集合中提供的获取所有Entry对象·的方法:
    public Set> entrySet(): 获取Map集合中所有的键值对对象的集合(Set集合)。

13.4 HashMap存储自定义类型键值

  • Map集合要保证key的唯一性,必须重写hashCode方法和equals方法
@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

13.5 LinkedHashMap类

在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构,有序

13.6 Hashtable集合

java.util.Hashtable implements Map

  • Hashtable:底层是一个哈希表,是一个线程安全的集合,单线程,速度慢
  • HashMap:底层是一个哈希表,是一个线程不安全的集合,多线程,速度快
  • Hashtable集合,不能存储null值、null键
  • (Hashtable,Vector)—>(HashMap,ArrayList)
  • Properties extends Hashtable 唯一和IO流相结合的集合

13.7 JDK9对集合添加的优化

Java 9,添加了几种集合工厂方法,更方便创建少量元素的集合/map实例。新的List、Set、Map的静态工厂方法可以更方便地创建集合的不可变实例。

  • of()方法只是Map,List,Set这三个接口的静态方法,其父类接口和子类实现并没有这类方法,比如
    HashSet,ArrayList等
  • 返回的集合是不可变的

14.异常

  • 异常 :程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
  • 在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理
  • 异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

14.1 异常体系

  • 异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable,其下有两个子类:java.lang.Error与java.lang.Exception,平常所说的异常指java.lang.Exception。
  • Error:严重错误,无法通过处理的错误,只能事先避免(内存溢出、系统崩溃)。
  • Exception:表示异常,产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。JVM打印异常对象,默认调用此方法
    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都要使用printStackTrace()。
  • public String getMessage():获取发生异常的原因。

14.2 异常分类

日常讲的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:

  • 编译时期异常:checkedException在编译时期检查,若没有处理,编译失败(如日期格式化异常)。
  • 运行时期异常:runtimeException在运行时期,检查异常.编译时期,运行异常不会编译器检测(如数学异常)。

14.3 异常的产生过程解析

JVM检测到异常后

  1. JVM根据异常产生的原因创建一个相应类的异常对象,其中包含异常产生的内容、原因、位置。
  2. 在出现异常的方法中,如果没有异常的处理逻辑(try…catch),那么JVM就会把异常对象抛出给方法的调用方法来处理这个异常,调用方法接收到该异常对象时,如也没有(try…catch),就会向上抛直到有其处理逻辑或到达JVM,JVM接收到这个异常对象后,会打印异常对象(内容、原因、位置),执行中断处理。

14.4 异常的处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

14.4.1 抛出异常throw

throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:throw new 异常类名(参数);

注意事项:

  • throw关键字必须写在方法的内部
  • throw关键字new的对象必须是Exception/Exception的子类
  • throw抛出指定的异常后:
    RuntimeException/RuntimeException的子类:可以默认交给JVM处理
    checkedException:throws(声明处理)/try…catch(捕获处理)

14.4.2 Objects非空判断

Objects类由一些静态的实用方法组成,这些方法是null-save(空指针安全)或null-tolerant(容忍空指针),在它的源码中,对对象为null的值进行了抛出异常操作。

public static <T> T requireNonNull(T obj) {
    if (obj == null)
          throw new NullPointerException();
    return obj;
}

14.4.3 声明异常throws

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了checkedException,而没有捕获处理,那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明上,表示当前方法不处理异常,而是提醒该方法的调用者来处理异常。

声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }

注意事项:

  • throws声明的异常必须是Exception/Exception的子类
  • 方法内部throw的异常对象,都要throws
  • 如果抛出的异常有子父类关系,直接声明父类异常即可
  • 调用了一个声明异常的方法,就必须处理该异常,要么继续throws交给上层调用者,要么捕获处理

14.4.4 捕获异常try…catch

如果异常throws至JVM后,会立刻终止程序
异常处理:

  • throws:该方法不处理,而是声明抛出,由该方法的调用者来处。
  • try-catch:捕获异常进行处理。

捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。

try{
     可能会产生异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}
  • try中可能抛出多个异常对象,那么就需要使用多个catch来处理这些对象
  • 处理完catch中的处理逻辑之后,继续执行try…catch之后的代码

try:该代码块中编写可能产生异常的代码。
catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。
try和catch都不能单独使用,必须连用

Throwable类中定义了一些查看方法:

  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。
  • public String toString():获取异常的类型和异常描述信息(不用)。
  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。

​ 包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

14.4.5 finally 代码块

  • finally:有一些特定的代码无论异常是否发生,都需要执行。而因为异常引发程序跳转,导致有些语句执行不到。finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
  • 语法:try…catch….finally
  • finally一般用于资源释放(资源回收)
  • 只有在try或者catch中调用退出JVM的相关方法,finally才不会执行,否则finally永远会执行。

14.4.6 异常注意事项

多个异常:

  • 针对每个异常写一个try…catch
  • 针对多个异常写一个try…catch
  • 针对多个异常写一个try,多个catch(一次捕获,多次处理)

要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}
  • 避免finally中有return语句,否则将永远返回finally中的结果
  • RuntimeException可以不处理
  • 如果父类抛出了多个异常,子类重写父类方法时,可以抛出和父类相同的异常/父类异常的子类/不抛出异常。
  • 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。如果子类产生异常,只能捕获处理,不能声明抛出

14.4.7 自定义异常类

1.自定义一个编译期异常: 自定义类 并继承于java.lang.Exception。
2.自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException。

public class RegisterException extends Exception/* RuntimeException*/ {
    public RegisterException() {
        super();
    }

    public RegisterException(String s) {
        super(s);
    }

}

15.多线程

15.1 并发与并行

并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生。

  • 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,在单 CPU
    系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
  • 而在多处理器系统中,这些可以并发执行的程序便可以分配到多个处理器上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核
    CPU,便是多核处理器,核越多,并行处理的程序越多,电脑运行的效率就越高。
  • 注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

15.2 线程与进程

  • 进程:每个进程都有一个独立的内存空间,一个应用程序 可能需要启动多个进程,这是多进程服务。进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 程序是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态实体。而进程则不同,进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。进程是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。反映了一个程序在一定的数据集上运行的全部动态过程。
  • 进程和程序并不是一一对应的,一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程。而这一点正是程序无法做到的,由于程序没有和数据产生直接的联系,即使是执行不同的数据的程序,他们的指令的集合依然是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。一般来说,一个进程肯定有一个与之对应的程序,而且只有一个。而一个程序有可能没有与之对应的进程(因为它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程,即为多线程程序。
  • 线程的划分尺度小于进程,使得多线程程序的并发性更高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

一个程序运行后至少有一个进程,一个进程中可以包含多个线程

  • 线程调度:
  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)
    Java使用抢占式调度。
  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程;多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

15.3 创建线程类

  • 主线程:当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少有一个主线程

  • 主线程的重要性体现在两方面:
    1.是产生其他子线程的线程;
    2.通常它必须最后完成执行(比如执行各种关闭动作)。

  • Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

  • 每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。

  • Java使用线程执行体来代表这段程序流。

  • Java中通过继承Thread类来创建并启动多线程的步骤如下:
    1.定义Thread类的子类,并重写该类的run()方法[线程执行体]。
    2.创建线程对象
    3.调用线程对象的start()方法来启动该线程

  • 调用start()方法的结果是两个线程并发的执行

  • void start()使该线程开始执行;java虚拟机调用该线程的run()方法

  • 多次start()一个线程是非法的。通过Thread实例的start(),一个Thread的实例只能产生一个线程。一个Thread的实例一旦调用start()方法,这个实例的started标记就标记为true,事实中不管这个线程后来有没有执行到底,只要调用了一次start()就再也没有机会运行了。从new到等待运行是单行道,所以如果对一个已经启动的线程对象再调用一次start方法的话,会发生IllegalThreadStateException异常.
    可以被重复调用的是run()方法。

  • Thread类中run()和start()方法的区别如下:
    run()方法: 在本线程内调用该Runnable对象的run()方法,可以重复多次调用
    start()方法: 启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程

  • 多线程执行时,在栈内存中,每一个执行线程都有一片自己所属独立的的栈内存空间。进行方法的压栈和弹栈。

  • JVM完成main线程的创建,开辟main线程的栈空间

  • start()通知JVM创建新线程,并开辟另一块独立的栈空间供该线程使用;在此栈空间中执行run()

  • 当执行线程的任务结束了,该线程就会被JVM在栈内存中释放。所有的执行线程都结束时,进程结束。

 public class MyThread extends Thread{
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在执行!"+i);
        }
    }
}

public class demoo {
    public static void main(String[] args) {
        MyThread m=new MyThread("new");
        m.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("hi"+i);
        }
    }
}

15.4 Thread类

构造方法:

  • public Thread():分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  • public String getName() :获取当前线程名称。
  • public void setName():设置线程名称
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用
  • public void start() :开启线程,Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

15.5 Runnable接口

采用 java.lang.Runnable接口创建线程

  • 定义Runnable接口的实现类,并重写run()方法
  • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动线程。
public class MyRunable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class demoo {
    public static void main(String[] args) {
        //MyThread m=new MyThread("new");
        MyRunable t=new MyRunable();
        Thread m=new Thread(t,"hello");
        m.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("hi"+i);
        }
    }
}
  • Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。
    而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
  • 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
  • 每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM都是在操作系统中启动了一个进程。
  • 如果一个类继承Thread类,则不适合资源共享。但实现Runable接口的话则很容易实现资源共享。
  • 实现Runnable接口比继承Thread类所具有的优势:
    1. 适合多个执行相同程序代码的线程去共享同一个资源。
    2. 可以避免java中单继承的局限性。
    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

15.6 匿名内部类实现线程的创建

匿名内部类:

  • 把子类继承父类,重写父类方法,创建子类对象一步完成
  • 把实现类实现接口,重写接口方法,创建实现类对象一步完成
 public class NoNameInnerClassThread {
   public static void main(String[] args) {            
// new Runnable(){      
// public void run(){          
// for (int i = 0; i < 20; i++) {              
// System.out.println("hi:"+i);                  
// }              
// }            
//    }; //‐‐‐这个整体  相当于new MyRunnable()    
        Runnable r = new Runnable(){
            public void run(){
                for (int i = 0; i < 20; i++) {
                   System.out.println("hr:"+i);  
                }
            } 
        };
        new Thread(r).start();
         for (int i = 0; i < 20; i++) {
           System.out.println("hu:"+i);  
        }
   } 
}

16.线程安全

16.1 线程安全

  • 如果有多个线程在同时运行,而这些线程可能会同时运行某段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  • 线程安全问题一般是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这些变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能产生线程不安全

16.2 线程同步

  • 要解决多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,保证每个线程都能正常执行原子操作,Java中提供了线程同步机制synchronized
  • 线程1进入操作的时候,线程2和线程3只能在外等着,线程1操作结束,线程2和线程3才有机会进入代码执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的问题
  • 有三种方式完成同步操作:
    同步代码块
    同步方法
    锁机制

16.2.1 同步代码块

  • 同步代码块:synchronized关键字用于方法中的某个区块上,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(锁对象/同步锁/对象锁/对象监视器){
     需要同步操作的代码 
}
  • 必须保证多个线程使用的锁对象是同一个
  • 同步代码块中的锁对象,可以是任意的对象
  • 锁对象可以把同步代码块锁住,只允许一个线程在同步代码块中执行
  • 注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能BLOCKED
  • 程序频繁的判断锁,获取锁,释放锁,程序的效率会降低

16.2.2 同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法时,其他线程无法进入

格式:

public synchronized void method(){ 
    //可能会产生线程安全问题的代码 
}
  • 同步锁就是该RunnableImpl实例
public class Ticket implements Runnable {
    private  int ticket=100;
    Object obj=new Object();
    @Override
    public void run() {
        while (true){
            sellticket();
        }
    }

    private synchronized void sellticket() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在卖第 " + ticket-- + " 张票");

        }
    }
}

public class RunnableImpl implements Runnable {
    private static int ticket = 100;
    //静态访问静态

    @Override
    public void run() {
        while (true) {
            payTicket();
        }
    }

    public static synchronized void payTicket() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在卖第 " + ticket-- + " 张票");

        }
    }
}
  • 静态方法的同步锁不能是this(this是创建对象后才产生的,静态方法要优先于对象
  • 静态方法的锁对象是本类的class属性–>class文件对象(反射)
  • 对于非static方法,同步锁就是this
  • 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)

16.2.3 Lock锁

  • java.util.concurrent.locks.Lock(接口)机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
    public void lock():加同步锁。
    public void unlock() :释放同步锁。

  • java.util.concurrent.locks.ReentrantLock implements Lock

17.线程的状态

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待 )一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

17.1 Timed Waiting

  • Thread.sleep(long m)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
  • 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  • 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  • sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
  • sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就
    开始立刻执行。
  • object.wait(long timeout)在其他线程调用此对象的 notify() 方法或 notifyAll()方法,或者超过指定的时间量前,导致当前线程等待。
  • wait方法,线程释放锁,进入WAITING或TIMED_WAITING状态(进入wait set中)。等待时间到了或被notify/notifyall唤醒后(进入ready queue中),回去竞争锁,如果获得锁,进入RUNNABLE,否则进入BLOCKED状态等待获取锁(entry set)。

17.2 Blocked

  • 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
    比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

17.3 Waiting

  • 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
  • object.wait() 在其他线程调用此对象的notify()方法或notifyAll()方法前,导致当前线程等待
  • object.notify()唤醒此对象监视器上等待的单个线程
    选取所通知对象的 wait set 中的一个线程释放
  • object.notifyAll()唤醒在此对象监视器上等待的所有线程。
    释放所通知对象的 wait set 上的全部线程。
  • wait()和notify()方法所对应的对象为同步锁
    线程之间的通信,通信间的两个线程必须使用同步代码块或同步函数,保证等待和唤醒不同时发生
    锁对象可以是任意对象,wait方法与notify方法是属于Object类的方法
  • 一个调用了某个对象的object.wait()方法的线程会等待另一个线程调用此对象的object.notify()方法或object.notifyAll()方法。

18.等待唤醒机制

18.1 线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。便需要线程通信来帮助解决线程之间对同一份资源的使用或操作,避免争夺。通过等待唤醒机制使各个线程能有效的利用资源。

18.2 等待唤醒机制

其实就是经典的生产者与消费者问题

19.线程池

19.1 线程池概念

线程池是一个可以容纳多个线程的容器,其中的线程可以反复使用。

  • 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,导致服务器死机。

19.2 线程池的使用

  • Java线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。
  • 要配置一个线程池是比较复杂的,在对于线程池的原理不是很清楚的情况下,可能配置的线程池并不是较优的。
  • 在java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。
    官方建议使用Executors工程类来创建线程池对象。

Executors中创建线程池的方法:

  • public static ExecutorService newFixedThreadPool(int nThreads)返回线程池对象(有界)
    返回ExecutorService接口的实现类对象,使用接口接收(面向接口编程)

ExecutorService中使用线程池的方法:

  • public Future submit(Runnable task)获取线程池中的某一个线程对象,提交Runnable任务用于执行,并返回一个表示该任务的Futrue
    Future接口:用来记录线程任务执行完毕后产生的结果

void shutdown()启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池(一般不做,void shutdown()启动一次顺序关闭,执行以前提交的任务,但不接受新任务)

20.Lambda表达式

20.1 函数式编程思想概述

做什么,而不是怎么做
面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情
函数式编程思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

20.2 冗余的Runnable代码->体验Lambda的更优写法

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用 java.lang.Thread类来启动该线程。
传统写法

public class demo2 {
    public static void main(String[] args) {
        //匿名内部类
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("多线程任务启动");

            }
        };
        new Thread(task).start();
    }
}

代码分析
对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在。

体验Lambda的更优写法
借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class demo2 {
    public static void main(String[] args) {
        new Thread(()-> System.out.println(Thread.currentThread().getName()+"new")).start();
        new Thread(()-> System.out.println(Thread.currentThread().getName()+"new")).start();
    }
}

20.3 Lambda标准格式

格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:
(参数类型 参数名称) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

举个例子

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}




import java.util.Arrays;
import java.util.Comparator;

public class demo {
    public static void main(String[] args) {
        Person[] arr={
                new Person("as", 19),
                new Person("ba", 18),
                new Person("cd", 20) };
        // 匿名内部类
        /*Comparator comp=new Comparator() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        };
        Arrays.sort(arr,comp);*/
        Arrays.sort(arr,(Person o1,Person o2)->{
            return o1.getAge()-o2.getAge();
        });

        for (Person person : arr) {
            System.out.println(person);
        }
    }
}

public interface Calculator {
    int calc(int a, int b);
}


public class demm {
    public static void main(String[] args) {
        invokeCalc(34,59,(int a,int b)->{
            return a+b;
        });
    }
    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("结果是:" + result);
    }
}

20.4 Lambda省略格式&Lambda的使用前提

省略规则

  • 小括号内参数的类型可以省略
  • 小括号内有且仅有一个参数,则小括号可以省略
  • 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号(必须一起省略)。
invokeCalc(34,59,(a,b)->a+b);

Lambda的使用前提

  • 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的 Runnable 、 Comparator
    接口还是自定义的接口,只有当接口中的抽象方法存在且唯一 时,才可以使用Lambda。
  • 使用Lambda必须具有上下文推断
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

21.File类

java.io.File类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作

  • 静态成员变量
    pathSeparator/pathSeparatorChar路径分割符,win(;)、linux( : )
    separator/separatorChar默认名称分隔符,windows()、linux(/)
  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径
  • 构造方法
    public File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的File实例。
    public File(String parent, String child)从父路径名字符串和子路径名字符串创建新的File实例。
    public File(File parent, String child)从父抽象路径名和子路径名字符串创建新的File实例。
  • 一个File对象代表硬盘中实际存在的一个文件或者目录。 无论该路径下是否存在文件或者目录,都不影响File对象的创建。
  • 不区分大小写
  • windows用反斜杠作为分隔符,反斜杠是转义字符,两个反斜杠代表一个

21.1 常用方法

21.1.1 获取功能的方法

  • public String getAbsolutePath()返回此File的绝对路径名字符串
  • public String getPath()将此File转换为路径名字符串
  • public String getName()返回由此File表示的文件或目录的名称
  • public long length()返回由此File表示的文件的长度,以字节为单位(空文件或文件夹为0)

21.1.2 判断功能的方法

  • public boolean exists()此File表示的文件或目录是否实际存在。
  • public boolean isDirectory()此File表示的是否为目录。
  • public boolean isFile()此File表示的是否为文件。

21.1.3 创建/删除功能的方法

  • public boolean mkdir()创建由此File表示的目录(单级文件夹)。
  • public boolean mkdirs()创建由此File表示的目录,包括任何必需但不存在的父目录。
  • public boolean createNewFile()当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete()删除由此File表示的文件或目录。(delete方法,如果此File表示目录,则目录必须为空才能删除)

21.2 目录的遍历

  • public String[] list()返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles()返回一个File数组,表示该File目录中的所有的子文件或目录。

遍历不存在的目录或者文件 NullPointerException

21.3 文件过滤器

  • java.io.FileFilter是一个接口,用于抽象路径名(File对象)的过滤器。
    boolean accept(File pathname)测试指定抽象路径名是否应该包含在某个路径名列表中。
  • java.io.FilenameFilter实现此接口的类实例可用于过滤器文件名。
    boolean accept(File dir, String name)测试指定文件是否应该包含在某一文件列表中
  • 接口作为参数,需要传递子类对象,重写其中方法。
  • accept方法,参数为File,表示当前File下所有的子文件和子目录。返回true保留,返回false过滤。

File类中:

  • File[] listFiles(FileFilter filter)返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
  • File[] listFiles(FilenameFilter filter)返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。

举个例子
文件搜索优化版

public class FileFilterImp implements java.io.FileFilter {
    @Override
    public boolean accept(File pathname) {
        return pathname.getName().endsWith(".doc")||pathname.isDirectory();
    }
}


public class demo {
    public static void main(String[] args) {
        File f=new File("E:\\软件");
        getAllFile(f);
    }
    public static void getAllFile(File n){
        //System.out.println(n);
        File[] f=n.listFiles(new FileFilterImp());//传递过滤器对象
        for (File file : f) {
            if(file.isDirectory()){
                getAllFile(file);
            }else{
                System.out.println(file);
            }
        }
    }
}

/*File[] files = dir.listFiles((File d,String name)->{
            return new File(dir, name).isDirectory() || name.toLowerCase().endsWith(".md");
        });*/

22.递归

  • 递归指在当前方法内调用自己的这种现象。
  • 递归的分类:
    直接递归称为方法自身调用自己。
    间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。
  • 注意事项:
    递归一定要有限定条件,保证递归能够停止下来,否则会发生栈内存溢出。
    在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
    构造方法禁止递归
  • 使用递归必须明确:递归结束的条件,递归的目的

举个例子
文件搜索

import java.io.File;

public class dihui {
    public static void main(String[] args) {
        File f=new File("E:\\软件");
        getAllFile(f);
    }
    public static void getAllFile(File n){
        //System.out.println(n);
        File[] f=n.listFiles();
        for (File file : f) {
            if(file.isDirectory()){
                getAllFile(file);
            }
            //String k=file.toString();
            //String k=file.getName();
            //String k=file.getPath();
            //if(k.endsWith(".doc"))
            //System.out.println(k);
            if(file.getName().endsWith(".doc"))
            System.out.println(file);
        }
    }
}

文件搜索优化版

public static void getAllFile(File n){
        //System.out.println(n);
        File[] f=n.listFiles((pathname)->pathname.getName().endsWith(".doc")||pathname.isDirectory());
        for (File file : f) {
            if(file.isDirectory()){
                getAllFile(file);
            }else{
                System.out.println(file);
            }
        }
    }

23.IO概述

  • Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
  • 根据数据的流向分为:输入流和输出流。
    输入流 :把数据从其他设备上读取到内存中的流。
    输出流 :把数据从内存 中写出到其他设备上的流。
  • 格局数据的类型分为:字节流和字符流。
    字节流 :以字节为单位,读写数据的流。
    字符流 :以字符为单位,读写数据的流。
  • 硬盘——>内存(输入)
    硬盘<——内存(输出)

顶级父类

输入流 输出流
字节流 字节输入流InputStream 字节输出流OutputStream
字符流 字符输入流Reader 字符输出流Writer

24.字节流

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都是一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

24.1 字节输出流【OutputStream】

java.io.OutputStream抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入len字节,从偏移量off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。

close方法,当完成流的操作时,须调用此方法,释放系统资源。

24.1.1 FileOutputStream类

  • OutputStream最简单的一个子类,java.io.FileOutputStream类,是文件输出流,用于将数据写出到文件
    写入数据的原理(内存—>硬盘):java程序—>JVM—>OS—>OS调用写数据的方法—>把数据写入到文件中

构造方法

  • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
    目的地是一个文件
  • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。
    目的地是一个文件的路径

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。

写出字节数据

  • 写出字节:write(int b) 方法,每次可以写出一个字节数据
public class demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("E:\\a.txt");
        fos.write(98);//b
        fos.close();
    }
}
  • 写出字节数组:write(byte[] b),每次可以写出数组中的数据
public class demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("E:\\a.txt");
        byte[] a="哈哈哈".getBytes();
        fos.write(a);
        fos.close();
    }
}
  • 写出指定长度字节数组:write(byte[] b, int off, int len) ,每次写出从off索引开始,len个字节
public class demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("E:\\a.txt");
        byte[] a="哈哈哈".getBytes();
        fos.write(a,0,3);//哈
        fos.close();
    }
}

数据追加续写

  • public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的File对象表示的文件。
  • public FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件。

这两个构造方法,参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。

public class demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("E:\\a.txt",true);
        byte[] a="哈哈哈".getBytes();
        fos.write(a);
        fos.close();
    }
}

写出换行
Windows系统里,换行符号是\r\n (回车符:回到一行的开头(return)。换行符:下一行(newline))
Unix:\n
Mac:\r,从 Mac OS X开始与Linux统一

public class demo {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("E:\\a.txt",true);
        byte[] a="哈哈哈".getBytes();
        for (int i = 0; i < 5; i++) {
            fos.write(a);
            fos.write("\r\n".getBytes());
        }
        fos.close();
    }
}

24.2 字节输入流【InputStream】

java.io.InputStream抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read(): 从输入流读取数据的下一个字节。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

24.2.1 FileInputStream类

java.io.FileInputStream类是文件输入流,从文件中读取字节。

构造方法

  • FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的 File对象 file命名。
  • FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

当创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException 。

读取字节数据

  • 读取字节:read方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1
    读完一个后,指针会自动向后移动
public class demo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("E:\\a.txt");
        int len = fis.read();
        System.out.println(len);
        int leo = fis.read();
        System.out.println(leo);
        fis.close();
    }
}
public class demo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("E:\\a.txt");
        int len=0;
        while ((len=fis.read())!=-1){
            System.out.println(len);
        }
        fis.close();
    }
}
  • 使用字节数组读取:read(byte[] b),每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1
public class demo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("E:\\a.txt");
        int len=0;
        byte[] b = new byte[5];
        while ((len=fis.read(b))!=-1){
            System.out.println(new String(b,0,len));//读取有效字节个数
        }
        fis.close();
    }
}

String类构造方法:
String(byte[] bytes) :把字节数组转换为字符串
String(byte[] bytes, int offset, int length) :把字节数组的一部分转换为字符串,offset:数组开始的索引 转换的字节个数

文件复制

public class demo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("E:\\a.txt");
        FileOutputStream fos = new FileOutputStream("D:\\a.txt");
        byte[] b = new byte[1024];
        int len;
        while ((len = fis.read(b))!=-1) {
            fos.write(b, 0 , len);
        }
        fos.close();
        fis.close();
    }
}

25.字符流

当使用字节流读取文本文件,遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。(GBK:2个字节;UTF-8:3个字节)

25.1 字符输入流【Reader】

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

25.1.1 FileReader类

java.io.FileReader类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。
idea中UTF-8
字节缓冲区:一个字节数组,用来临时存储字节数据。

构造方法

  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。

读取字符数据

  • 读取字符:read方法,每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1,循环读取
public class demo {
    public static void main(String[] args) throws IOException {
        FileReader fr=new FileReader("E:\\a.txt");
        int len = 0;//虽然读取了一个字符,但是会自动提升为int类型
        while ((len=fr.read())!=-1){
            System.out.print((char)len);
        }
        fr.close();
    }
}
  • 使用字符数组读取:read(char[] cbuf),每次读取b的长度个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回-1
public class demo {
    public static void main(String[] args) throws IOException {
        FileReader fr=new FileReader("E:\\a.txt");
        int len = 0;
        char[] cbuf = new char[8];
        while ((len=fr.read(cbuf))!=-1){
            System.out.print(new String(cbuf,0,len));
        }
        fr.close();
    }
}

String(char[] value)
String(char[] value, int offset, int count)

25.2 字符输出流【Writer】

java.io.Writer抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • void write(int c) 写入单个字符。
  • void write(char[] cbuf)写入字符数组。
  • abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str)写入字符串。
  • void write(String str, int off, int len)写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
  • void flush()刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

25.2.1 FileWriter类

java.io.FileWriter类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

构造方法

  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。

基本写出数据

  • 写出字符:write(int b) 方法,每次可以写出一个字符数据
public class demo {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("E:\\b.txt");
        fw.write(97);
        fw.flush();//刷新缓冲区,流对象可以继续使用      
        fw.write('a');
        fw.write('l');
        fw.write(30000);
        fw.close();
    }
}

虽然参数为int类型四个字节,但是只会保留一个字符的信息写出。
未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。

关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush方法了。

  • flush :刷新缓冲区,流对象可以继续使用。
  • close:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
  • 写出字符数组 :write(char[] cbuf) 和 write(char[] cbuf, int off, int len),每次可以写出字符数组中的数据,用法类似FileOutputStream
public class demo {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("E:\\.txt");
        char[] chars = "sw速度按市场".toCharArray();
        fw.write(chars);
        fw.write(chars,2,2);//写一部分
        //fw.flush();
        fw.close();
    }
}
  • 写出字符串:write(String str) 和 write(String str, int off, int len) ,每次可以写出字符串中的数据,更为方便
public class demo {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("E:\\a.txt");
        String chars = "sw速度按市场";
        fw.write(chars);
        fw.write(chars,2,2);
        //fw.flush();
        fw.close();
    }
}

续写和换行
操作类似于FileOutputStream

public class demo {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("E:\\a.txt",true);
        String c = "市场";
        for (int i = 0; i < 10; i++) {
            fw.write(c+i+"\r\n");
        }
        fw.close();
    }
}

字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流。

26.IO异常的处理

实际开发中并不一直把异常抛出这样处理,建议使用try…catch…finally 代码块,处理异常部分

JDK7前处理

public class HandleException1 {
    public static void main(String[] args) {
          // 声明变量
        FileWriter fw = null;
        try {
            //创建流对象
            fw = new FileWriter("fw.txt");
            // 写出数据
            fw.write("哈哈哈"); 
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

JDK7的处理
还可以使用JDK7优化后的try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。

public class HandleException2 {
    public static void main(String[] args) {
          // 创建流对象
        try ( FileWriter fw = new FileWriter("fw.txt"); ) {
            // 写出数据
            fw.write("哈"); 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

JDK9的改进
JDK9中try-with-resource 的改进,对于引入对象的方式,支持的更加简洁。被引入的对象,同样可以自动关闭,无需手动close,我们来了解一下格式。

public class TryDemo {
    public static void main(String[] args) throws IOException {
           // 创建流对象
        final  FileReader fr  = new FileReader("in.txt");
        FileWriter fw = new FileWriter("out.txt");
           // 引入到try中
        try (fr; fw) {
              // 定义变量
            int b;
              // 读取数据
              while ((b = fr.read())!=-1) {
                // 写出数据
                fw.write(b);
              }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

27.属性集

java.util.Properties 继承于Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。
唯一一个和IO流相结合。
Properties集合中的方法story,把集合中的临时数据,持久化写入到硬盘存储
Properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用

27.1 Properties类

构造方法

  • public Properties() :创建一个空的属性列表。

基本的存储方法

  • public Object setProperty(String key, String value) : 保存一对属性。
  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
  • public Set stringPropertyNames() :所有键的名称的集合(键值)。相当于Map中的keyset
public class demm {
    public static void main(String[] args) {
        Properties prop=new Properties();
        prop.setProperty("ss","155");
        prop.setProperty("sd","154");
        prop.setProperty("ssg","135");
        Set<String> set=prop.stringPropertyNames();
        for (String s : set) {
            String va = prop.getProperty(s);
            System.out.println(s+va);
        }
    }
}

与流相关的方法

  • public void store(OutputStream outStream, String comments):把集合中的临时数据,持久化写入到硬盘存储。
    字节输出流,不能写入中文
  • public void store(Writer writer, String comments)
    字节输出流,不能写入中文
    String comments:注释,用来解释说明保存的文件是做什么用的,不能使用中文,会产生乱码,默认Unicode编码,一般使用空字符串
  • public void load(InputStream inStream): 从字节输入流中读取键值对。//不能中文
  • public void load(Reader reader)
public static void main(String[] args) throws IOException {
        Properties prop=new Properties();
        prop.setProperty("ss","155");
        prop.setProperty("sd","154");
        prop.setProperty("ssg","135");
        FileWriter fw=new FileWriter("E:\\a.txt");
        prop.store(fw,"save");
        fw.close();
    }
public static void main(String[] args) throws IOException {
        Properties prop=new Properties();
        prop.load(new FileInputStream("E:\\a.txt"));
        Set<String> set = prop.stringPropertyNames();
        for (String s : set) {
            String va = prop.getProperty(s);
            System.out.println(s+va);
        }
    }

文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

28.缓冲流

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流:BufferedInputStream,BufferedOutputStream
  • 字符缓冲流:BufferedReader,BufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

28.1 字节缓冲流

构造方法

  • public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
  • BufferedInputStream(InputStream in, int size) :创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。
  • public BufferedOutputStream(OutputStream out, int size) :创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

基本方法与普通字节流调用方式一致

28.2 字符缓冲流

构造方法

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedReader(Reader in, int sz) :创建一个使用指定大小输入缓冲区的缓冲字符输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。
  • public BufferedWriter(Writer out, int sz) :创建一个使用给定大小输出缓冲区的新缓冲字符输出流。
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

基本方法与普通字符流调用方式一致
特有方法

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。
public static void main(String[] args) throws IOException {
        BufferedReader bw=new BufferedReader(new FileReader("E:\\a.txt"));
        String line=null;
        while ((line=bw.readLine())!=null){
            System.out.println(line);
            System.out.println("========");
        }
        bw.close();
    }
public static void main(String[] args) throws IOException {
        BufferedWriter bw=new BufferedWriter(new FileWriter("E:\\a.txt",true));
        bw.write("sHd从");
        bw.newLine();
        bw.write("sHd从");
        bw.close();
    }

29.转换流

29.1 字符编码和字符集

字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

  • 编码:字符(能看懂的)–字节(看不懂的)
  • 解码:字节(看不懂的)–>字符(能看懂的)
  • 字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。
  • 编码表:生活中文字和计算机中二进制的对应规则

字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。
指定了编码,它所对应的字符集自然就指定了。

ASCII字符集 :

  • ASCII(American Standard Code for Information
    Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
  • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

ISO-8859-1字符集:

  • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
  • ISO-8859-1使用单字节编码,兼容ASCII编码。

GBxxx字符集:

  • GB就是国标的意思,是为了显示中文而设计的一套字符集。
  • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
  • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
  • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。

Unicode字符集 :

  • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
  • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
  • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
    • 128个US-ASCII字符,只需一个字节编码。
    • 拉丁文等字符,需要二个字节编码。
    • 大部分常用字(含中文),使用三个字节编码。
    • 其他极少使用的Unicode辅助字符,使用四字节编码。

在IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,可能文件不是这个编码,就会出现乱码。

29.2 InputStreamReader类

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
public static void main(String[] args) throws IOException {
        InputStreamReader isd=new InputStreamReader(new FileInputStream("E:\\a.txt"));
        InputStreamReader isd2=new InputStreamReader(new FileInputStream("E:\\a.txt"),"GBK");
        int read;
        while ((read=isd.read())!=-1){
            System.out.print((char)read);
        }
        isd.close();
        while ((read=isd2.read())!=-1){
            System.out.print((char)read);
        }
        isd.close();
    }

29.3 OutputStreamWriter类

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName):创建一个指定字符集的字符流。
public static void main(String[] args) throws IOException {
        OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("E:\\b.txt"));
        osw.write("好是是");
        osw.close();
        OutputStreamWriter ow2=new OutputStreamWriter(new FileOutputStream("E:\\a.txt"),"GBK");
        ow2.write("好是是");
        ow2.close();
    }

30.序列化

  • Java提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
  • 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。

30.1 ObjectOutputStream类

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

构造方法

  • public ObjectOutputStream(OutputStream out):创建一个指定OutputStream的ObjectOutputStream。

构造举例,代码如下:

FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

序列化操作
1.一个对象要想序列化,必须满足两个条件:

  • 该类必须实现java.io.Serializable 接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException 。
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
public class Person implements java.io.Serializable{
    private String name;
    private transient int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

2.写出对象方法

  • public final void writeObject (Object obj) : 将指定的对象写出。
public static void main(String[] args) {
        Person person = new Person();
        person.setAge(18);
        person.setName("美铝呀");
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:\\a.txt"));
            out.writeObject(person);
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

30.2ObjectInputStream类

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

构造方法

  • public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream。

反序列化操作1
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

public final Object readObject () : 读取一个对象。

public static void main(String[] args) { 
        Person p=null;
        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(new FileInputStream("E:\\a.txt"));
            p=(Person) in.readObject();
            in.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(p);//Person{name='美铝呀', age=0},age没有被序列化
    }

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

反序列化操作2
当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name
     // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
     public int eid; 
    ...
}

31.打印流

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

31.1 PrintStream类

特点

  • 只负责数据的输出,不负责数据的读取
  • 与其他输出流不同,PrintStream永远不会抛出 IOexcertion
  • 有特有的方法,print方法和println方法

构造方法
public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。
public PrintStream(File file): 使用指定的文件名创建一个新的打印流。
public PrintStream(OutputStream out): 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

PrintStream ps = new PrintStream("ps.txt")

如果使用继承父类的write方法写数据,那么查看数据时会查看编码表 97->a
如果使用自己特有的方法,原样输出 97->97

public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("E:\\a.txt");
        ps.write(97);
        ps.println(97);
        ps.close();
    }

改变打印流向
System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。

public static void main(String[] args) throws FileNotFoundException {
        // 调用系统的打印流,控制台直接输出97
        System.out.println(97);
        // 创建打印流,指定文件的名称
        PrintStream ps = new PrintStream("E:\\\\a.txt");
        // 设置系统的打印流流向,输出到ps.txt
        System.setOut(ps);
        // 调用系统的打印流,ps.txt中输出97
        System.out.println(97);
        ps.close();
    }

32.网络编程入门

32.1 软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。
  • B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

32.2 网络通信协议

  • 网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
  • TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
  • TCP/IP协议中的四层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能。
    链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
    网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
    运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
    应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。

32.3 协议分类

  • 通信的协议还是比较复杂的,java.net包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • UDP:用户数据报协议(User Datagram Protocol)。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

  • 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

  • 但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。

  • 特点:数据被限制在64kb以内,超出这个范围就不能发送了。

  • 数据报(Datagram):网络传输的基本单位

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

  • 在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。

  • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
    第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
    第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
    第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。

  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

32.4 网络编程三要素

协议

  • 协议:计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

IP地址

  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。
  • IP地址分类
    IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
    IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。
  • 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
  • 常用命令
    查看本机IP地址,在控制台输入:ipconfig
    检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216
  • 特殊的IP地址
    本机IP地址:127.0.0.1localhost

端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

  • 端口号可以唯一标识设备中的进程(应用程序)。
  • 端口号:用两个字节表示的整数,它的取值范围是0-65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
  • 利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

常用的端口号
1.80端口 网络端口
2.数据库 mysql:3360 orcal:1521
3.Tomcat服务器:8080

33.TCP通信程序

TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

两端通信时步骤:
1.服务端程序,需要事先启动,等待客户端的连接。
2.客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
客户端与服务器端建立一个逻辑连接,这个连接中包含一个IO对象,通信的对象不仅仅是字符,所以IO对象是字节流对象。

在Java中,提供了两个类用于实现TCP通信程序:
1.客户端:java.net.Socket 类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
2.服务端:java.net.ServerSocket 类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。

33.1 Socket类

Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点,包含了IP地址和端口号的网络单位

构造方法

  • public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。
    String host:服务器主机的名称/服务器的IP地址
    int port:服务器的端口号

回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

成员方法

  • public InputStream getInputStream() : 返回此套接字的输入流。
    如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。
    关闭生成的InputStream也将关闭相关的Socket。
  • public OutputStream getOutputStream() : 返回此套接字的输出流。
    如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。
    关闭生成的OutputStream也将关闭相关的Socket。
  • public void close() :关闭此套接字。
    一旦一个socket被关闭,它不可再使用。
    关闭此socket也将关闭相关的InputStream和OutputStream 。
  • public void shutdownOutput() : 禁用此套接字的输出流。
    任何先前写出的数据将被发送,随后终止输出流。

客户端与服务器端进行交互,必须使用Socket中的提供的网络流,不能使用自己创建的流对象
当我们创建客户端对象Socket时,就会去请求服务器和服务器经过3次握手
此时服务器若没有启动,则会抛出异常

33.2 ServerSocket类

ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法

  • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。

成员方法

  • public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

33.3 简单的TCP网络程序

TCP通信分析图解
1.【服务端】启动,创建ServerSocket对象,等待连接。
2.【客户端】启动,创建Socket对象,请求连接。
3.【服务端】接收连接,调用accept方法,并返回一个Socket对象。
4.【客户端】Socket对象,获取OutputStream,向服务端写出数据。
5.【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。

到此,客户端向服务端发送数据成功。
自此,服务端向客户端回写数据。

6.【服务端】Socket对象,获取OutputStream,向客户端回写数据。
7.【客户端】Scoket对象,获取InputStream,解析回写数据。
8.【客户端】释放资源,断开连接。

客户端向服务器发送数据

  • 服务器端
public class server {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器");
        ServerSocket serverSocket=new ServerSocket(8888);
        Socket server = serverSocket.accept();
        InputStream in=server.getInputStream();
        byte[] b = new byte[1024];
        int len = in.read(b);
        String msg = new String(b, 0, len);
        System.out.println(msg);
        in.close();
        server.close();
    }
}
  • 服务器端
public class client {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端 发送数据");
        Socket socket=new Socket("127.0.0.1",8888);
        OutputStream outputStream=socket.getOutputStream();
        outputStream.write("你好服务器".getBytes());
        outputStream.close();
        socket.close();
    }
}

服务器向客户端回写数据

  • 服务器端
public class server {
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动 , 等待连接 .... ");
        ServerSocket ss = new ServerSocket(6666);
        Socket server = ss.accept();
        InputStream is = server.getInputStream();
        byte[] b = new byte[1024];
        int len = is.read(b);
        String msg = new String(b, 0, len);
        System.out.println(msg);
        OutputStream out = server.getOutputStream();
        out.write("我很好,谢谢你".getBytes());
        out.close();
        is.close();
        server.close();
    }
}
  • 客户端
public class client {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端 发送数据");
        Socket client = new Socket("localhost", 6666);
        OutputStream os = client.getOutputStream();
        os.write("你好么? tcp ,我来了".getBytes());
        InputStream in = client.getInputStream();
        byte[] b = new byte[100];
        int len = in.read(b);
        System.out.println(new String(b, 0, len));
        in.close();
        os.close();
        client.close();
    }
}

33.4 TCP文件上传案例

原理:客户端使用本地的字节输出流,把文件上传到服务器,服务器把上传的文件保存到服务器的硬盘上,

  1. 客户端使用本地字节输入流,读取要上传的文件
  2. 客户端使用网络字节输出流,把读取的的文件上传到服务器
  3. 服务器使用网络字节输出流,读取客户端上传的文件
  4. 服务器使用本地字节输出流,把读取到的文件,保存到服务器的硬盘上
  5. 服务器使用网络字节输出流,给客户端回写一个“上传成功”
  6. 客户端使用网络字节输入流,读取服务器回写的数据
  7. 释放资源

客户端和服务器和本地硬盘进行读写,需要使用自己创建的字节流对象(本地流)
客户端和服务器之间进行读写,必须使用Socket中提供的字节流(网络流)

文件上传的原理,就是文件的复制
明确:数据源,数据目的地

public class fileClient {
    public static void main(String[] args) throws IOException {
        BufferedInputStream bis  = new BufferedInputStream(new FileInputStream("E:\\a.txt"));
        Socket socket = new Socket("localhost", 6666);
        BufferedOutputStream bos   = new BufferedOutputStream(socket.getOutputStream());
        byte[] b  = new byte[1024 * 8 ];
        int len ;
        while (( len  = bis.read(b))!=-1) {
            bos.write(b, 0, len);
            bos.flush();
        }
        System.out.println("文件发送完毕");
        bos.close();
        socket.close();
        bis.close();
        System.out.println("文件上传完毕 ");
    }
}

public class fileServer {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动ing");
        ServerSocket socket=new ServerSocket(6666);
        Socket sk=socket.accept();
        BufferedInputStream bis = new BufferedInputStream(sk.getInputStream());
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\k.txt"));
        byte[] b = new byte[1024 * 8];
        int len;
        while ((len = bis.read(b)) != -1) {
            bos.write(b, 0, len);
        }
        sk.close();
        bis.close();
        bos.close();
        System.out.println("文件上传已保存");
    }
}

文件上传优化分析
1.文件名称写死的问题
服务端保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一(自定义)

FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 文件名称
BufferedOutputStream bos = new BufferedOutputStream(fis);

2.循环接收的问题
服务端只保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件

// 每次接收新的连接,创建一个Socket
whiletrue{
    Socket accept = serverSocket.accept();
    ......
}

3.效率问题
服务端在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化

whiletrue{
    Socket accept = serverSocket.accept();
    // accept 交给子线程处理.
    new Thread(() -> {
          ......
        InputStream bis = accept.getInputStream();
          ......
    }).start();
}
public class fileServer {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动ing");
        ServerSocket socket=new ServerSocket(6666);
        while(true){
            Socket sk=socket.accept();
            new Thread(() -> {
                try {
                    BufferedInputStream bis = new BufferedInputStream(sk.getInputStream());
                    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\"+System.currentTimeMillis()+".jpg"+".txt"));
                    byte[] b = new byte[1024 * 8];
                    int len;
                    while ((len = bis.read(b)) != -1) {
                        bos.write(b, 0, len);
                    }
                    bis.close();
                    bos.close();
                    System.out.println("文件上传已保存");
                }catch (IOException e){
                    System.out.println(e);
                }
            }).start();
        }
    }
}

信息回写

public class FileUpload_Server {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动.....  ");
        // 1. 创建服务端ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        // 2. 循环接收,建立连接
        while (true) {
            Socket accept = serverSocket.accept();
              /*
              3. socket对象交给子线程处理,进行读写操作
               Runnable接口中,只有一个run方法,使用lambda表达式简化格式
            */
            new Thread(() -> {
                try (
                    //3.1 获取输入流对象
                    BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
                    //3.2 创建输出流对象, 保存到本地 .
                    FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
                    BufferedOutputStream bos = new BufferedOutputStream(fis);
                ) {
                    // 3.3 读写数据
                    byte[] b = new byte[1024 * 8];
                    int len;
                    while ((len = bis.read(b)) != -1) {
                        bos.write(b, 0, len);
                    }
                    // 4.=======信息回写===========================
                    System.out.println("back ........");
                    OutputStream out = accept.getOutputStream();
                    out.write("上传成功".getBytes());
                    out.close();
                    //================================
                    //5. 关闭 资源
                    bos.close();
                    bis.close();
                    accept.close();
                    System.out.println("文件上传已保存");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
public class FileUpload_Client {
    public static void main(String[] args) throws IOException {
        // 1.创建流对象
        // 1.1 创建输入流,读取本地文件
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
        // 1.2 创建输出流,写到服务端
        Socket socket = new Socket("localhost", 6666);
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        //2.写出数据.
        byte[] b  = new byte[1024 * 8 ];
        int len ;
        while (( len  = bis.read(b))!=-1) {
            bos.write(b, 0, len);
        }
          // 关闭输出流,通知服务端,写出数据完毕
        socket.shutdownOutput();
        System.out.println("文件发送完毕");
        // 3. =====解析回写============
        InputStream in = socket.getInputStream();
        byte[] back = new byte[20];
        in.read(back);
        System.out.println(new String(back));
        in.close();
        // ============================
        // 4.释放资源
        socket.close();
        bis.close();
    }
}

34.函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实
底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部
类的“语法糖”,但是二者在原理上是不同的。

34.1 格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的 public abstract 是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {   
void myMethod();    
}

34.2 @FunctionalInterface注解

与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();    
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

34.3 自定义函数式接口

对于刚刚定义好的 MyFunctionalInterface 函数式接口,典型使用场景就是作为方法的参数:

public class Demo09FunctionalInterface {   
// 使用自定义的函数式接口作为方法参数    
private static void doSomething(MyFunctionalInterface inter) {    
inter.myMethod(); // 调用自定义的函数式接口方法        
}    
   
public static void main(String[] args) {    
// 调用使用函数式接口的方法        
doSomething(()> System.out.println("Lambda执行啦!"));        
}    
}

35.函数式编程

35.1 Lambda的延迟执行

有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
性能浪费的日志案例
注:日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:

public class Demo01Logger {
    private static void log(int level, String msg) {
        if (level == 1) {
           System.out.println(msg);  
        }
    }
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
        log(1, msgA + msgB + msgC);
    }
}

这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。

SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如: LOGGER.debug(“变量{}的取值为{}。”, “os”, “macOS”) ,其中的大括号 {} 为占位符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。

体验Lambda的更优写法

@FunctionalInterface
public interface MessageBuilder {
        String buildMessage();
}
private static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            System.out.println(builder.buildMessage());
        }
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
        log(1, ()->msgA + msgB + msgC);
    }

这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接。

35.2 使用Lambda作为参数和返回值

如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。

例如 java.lang.Runnable 接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。

public class demo {
    public static void startThread(Runnable run){
        new Thread(run).start();
    }

    public static void main(String[] args) {
        startThread(()->System.out.println(Thread.currentThread().getName()+"---->"+"线程启动"));
    }
}
private static Comparator<String> newComparator() {
       return (a, b)> b.length() ‐ a.length();  
    }
    public static void main(String[] args) {
        String[] array = { "abc", "ab", "abcd" };
        System.out.println(Arrays.toString(array));
        Arrays.sort(array, newComparator());
        System.out.println(Arrays.toString(array));
    }

36.常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。
下面是最简单的几个接口及使用示例。

36.1 Supplier接口

java.util.function.Supplier 接口仅包含一个无参的方法: T get() 。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

public class sa {
    private static String getString(Supplier<String> function) {
        return function.get();
    }
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(()-> msgA+msgB));
    }
}

36.2 Consumer接口

java.util.function.Consumer 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
抽象方法:accept
Consumer 接口中包含抽象方法 void accept(T t) ,意为消费一个指定泛型的数据。基本使用如:

public class sa {
        private static void consumeString(Consumer<String> function) {
            function.accept("Hello");
        }
        public static void main(String[] args) {
            consumeString(s-> System.out.println(s));
        }
}

默认方法:andThen
如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t)> { accept(t); after.accept(t); };
}

备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:

 private static void consumeString(Consumer<String> one, Consumer<String> two) {
        one.andThen(two).accept("Hello");
    }

    public static void main(String[] args) {
        consumeString(
                s-> System.out.println(s.toLowerCase()),
                s-> System.out.println(s.toUpperCase())
        );
    }

格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX。性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。

public static void main(String[] args) {
   String[] array = { "迪,女", "丽,女", "热巴,男" };  
}
private static void consumeString(Consumer<String> one, Consumer<String> two,String arr[]) {
        for (String s : arr) {
            one.andThen(two).accept(s);
        }
    }

    public static void main(String[] args) {
        String[] array = { "迪,女", "丽,女", "热巴,男" };
        consumeString(s-> System.out.print("姓名"+s.split(",")[0]),
        s-> System.out.println("。姓别"+s.split(",")[1]+"。"),array);
    }

36.3 Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate 接口。
抽象方法:test
Predicate 接口中包含一个抽象方法: boolean test(T t) 。用于条件判断的场景:

private static boolean pre(String s, Predicate<String> predicate) {
        return predicate.test(s);
    }

    public static void main(String[] args) {
        String array = "迪,女, 丽女热巴,男";
        boolean v=pre(array,(str)->str.length()>5);
        System.out.println(v);
    }

默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t)> test(t) && other.test(t);
}
private static boolean pre(String s, Predicate<String> one,Predicate<String> two) {
        return one.and(two).test(s);
        //return one.test(s)&&two.test(s);
    }

    public static void main(String[] args) {
        String array = "迪,女, 丽女热巴,男";
        boolean v=pre(array,(str)->str.length()>5,(str)->str.contains(","));
        System.out.println(v);
    }

默认方法:or
与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或”。JDK源码为:

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t)> test(t) || other.test(t);
}
private static boolean pre(String s, Predicate<String> one,Predicate<String> two) {
        return one.or(two).test(s);
        //return one.test(s)||two.test(s);
    }

    public static void main(String[] args) {
        String array = "迪,女, 丽女热巴,男";
        boolean v=pre(array,(str)->str.length()>5,(str)->str.contains("h"));
        System.out.println(v);
    }

默认方法:negate
“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的JDK源代码为:

default Predicate<T> negate() {
    return (t)> !test(t);
}
private static boolean pre(String s, Predicate<String> one) {
        return one.negate().test(s);
        //return !one.test(s);
    }

    public static void main(String[] args) {
        String array = "迪,女, 丽女热巴,男";
        boolean v=pre(array,(str)->str.length()>25);
        System.out.println(v);
    }

36.4 Function接口

java.util.function.Function 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
抽象方法:apply
Function 接口中最主要的抽象方法为: R apply(T t) ,根据类型T的参数获取类型R的结果。
使用的场景例如:将 String 类型转换为 Integer 类型。

private static void method(String s,Function<String, Integer> function) {
        int num = function.apply(s);
        System.out.println(num + 20);
    }

    public static void main(String[] args) {
        method("10",s->Integer.parseInt(s));
    }

默认方法:andThen
Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t)> after.apply(apply(t));
}
private static void method(String s,Function<String, Integer> one,Function<Integer, String> two) {
        String num = one.andThen(two).apply(s);
        System.out.println(num);
    }

    public static void main(String[] args) {
        method("10",s->Integer.parseInt(s)+10,i->i+"20");//2020
    }

37.Stream流

几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元
素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。
循环遍历的弊端

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行
了对比说明。

  • for循环的语法就是“怎么做”
  • for循环的循环体才是“做什么”

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从
第一个到最后一个顺次处理的循环
。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;
  2. 然后再根据条件二过滤为子集C。

在Java 8之前的做法可能为:

public static void main(String[] args) {  
       List<String> list = new ArrayList<>();  
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if (name.startsWith("张")) {
               zhangList.add(name);  
            }
        }
        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
               shortList.add(name);  
            }
        }
        for (String name : shortList) {
           System.out.println(name);  
        }
    }

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后进行对结果进行打印输出。

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循
环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使
用另一个循环从头开始。

Stream的更优写法

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        list.stream().filter(s->s.startsWith("张")).filter(s->s.length()==3).forEach(System.out::println);
    }

37.1 流式思想概述

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

Stream(流)是一个来自数据源的元素队列

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源流的来源。 可以是集合,数组 等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。
    这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。
    Stream提供了内部迭代的方式,流可以直接调用遍历方法。

当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结
,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以
像链条一样排列,变成一个管道。

37.2 获取流

java.util.stream.Stream 是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)

获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流。

举例

  • java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。
  • java.util.Map 接口不是 Collection的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况
  • 如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单
public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Stream<String> stream1=list.stream();

        Set<String> set=new HashSet<>();
        Stream<String> stream2 = set.stream();

        Map<String,String> map=new HashMap<>();
        Set<String> keySet=map.keySet();
        Stream<String> stream3=keySet.stream();
        //必须单列转换
        Collection<String> values=map.values();
        Stream<String> stream4=values.stream();

        Set<Map.Entry<String, String>> entries = map.entrySet();
        Stream<Map.Entry<String,String>> stream5=entries.stream();
        //数组
        Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
        //可变参数可以传递数组
        Integer[] arr={1,2,3,4,5};
        Stream<Integer> stream7=Stream.of(arr);
        String[] arr2={"s","df"};
        Stream<String> stream8=Stream.of(arr2);
    }

备注: of 方法的参数其实是一个可变参数,所以支持数组。

37.3 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方 法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 count 和 forEach 方法。

更多方法,请自行参考API文档。

逐一处理:forEach
虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同。void forEach(Consumer action);该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。遍历之后就不能继续调用Stream流中的其他方法。

public static void main(String[] args) {
        Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
        stream.forEach(name‐> System.out.println(name));
    }

过滤:filter
可以通过 filter 方法将一个流转换成另一个子集流。Stream filter(Predicate predicate);该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。

public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s ‐> s.startsWith("张"));
    }

Stream流属于管道流,只能被消费(使用)一次,第一个Stream流调用完毕方法,数据就会转到下一个Stream上,而这时第一个Stream流已经使用完毕,就会关闭,所以第一个Stream流不能再调用方法。

映射:map
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。 Stream map(Function mapper);该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

public static void main(String[] args) {
        Stream<String> original = Stream.of("10", "12", "18");
        Stream<Integer> result = original.map(str‐>Integer.parseInt(str));
    }

统计个数:count
正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数:long count();该方法返回一个long值代表元素个数(不再像旧集合那样是int值)

public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s ‐> s.startsWith("张"));
        System.out.println(result.count()); // 2
    }

取用前几个:limit
limit 方法可以对流进行截取,只取用前n个。Stream limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。

public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }

跳过前几个:skip
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:Stream skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }

组合:concat
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :static Stream concat(Stream a, Stream b)

这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。

public static void main(String[] args) {
        Stream<String> streamA = Stream.of("张无忌");
        Stream<String> streamB = Stream.of("张翠山");
        Stream<String> result = Stream.concat(streamA, streamB);
    }

38.方法的引用

38.1 方法引用符

双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
语义分析
例如, System.out 对象中有一个重载的 println(String) 方法恰好就是我们所需要的。那么对于printString 方法的函数式接口参数,对比下面两种写法,完全等效:

  • Lambda表达式写法: s -> System.out.println(s);
    语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。
  • 方法引用写法: System.out::println
    语义是指:直接让 System.out 中的 println 方法来取代Lambda。
    两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。

注:Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常

推导与省略
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

38.2 通过对象名引用成员方法

通过对象名引用成员方法,前提是对象名是已经存在的,成员方法也是已经存在的

public class Meth {
    public void printUpperCase(String s){
        System.out.println(s.toUpperCase());
    }
}

public interface printable {
    void print(String s);
}

public class hel {
    public static void printst(printable p){
        p.print("hello");
    }

    public static void main(String[] args) {
        /*printst((s)->{
            Meth obj=new Meth();
            obj.printUpperCase(s);
        });*/
        Meth obj=new Meth();
        printst(obj::printUpperCase);
    }
}

38.3 通过类名称引用静态方法

通过类名引用静态方法,前提类已经存在,静态成员方法也已经存在。

public static void printst(int p,printable s){
        System.out.println(s.print(p));
    }

    public static void main(String[] args) {
        printst(-10,s->Math.abs(s));
        printst(-10,Math::abs);
    }

38.4 通过super引用成员方法

super是已经存在的,父类的成员方法也是存在的,可以直接使用super引用父类成员方法

public interface printable {
    void greet();
}

public class Meth {
    public void sayhello(){
        System.out.println("Hello 我是:你爸");
    }
}

public class m extends Meth {
    @Override
    public void sayhello() {
        System.out.println("儿子");
    }
    public void me(printable g){
        g.greet();
    }
    public void show(){
        /*me(()->{
            Meth h=new Meth();
            h.sayhello();
        });*/
        //me(()->super.sayhello());
        me(super::sayhello);
    }

    public static void main(String[] args) {
        new m().show();
    }
}

38.5 通过this引用成员方法

this是已经存在的,本类的成员方法存在,可以直接使用this引用本类的成员方法

@FunctionalInterface
public interface printable {
    void buy();
}

public class Meth {
    public void buyhouse(){
        System.out.println("大别墅,爸");
    }
    public void marry( printable p){
        p.buy();
    }
    public void kuaile(){
        //marry(()->this.buyhouse());
        marry(this::buyhouse);
    }

    public static void main(String[] args) {
        new Meth().kuaile();
    }
}

38.6 类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。

public class People {
        private String name;
        public People(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
}

public interface PersonBuilder {
    People buildPerson(String name);
}

public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
    public static void main(String[] args) {
        printName("好的话", People::new);
    }

38.7 数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

public interface ArrayBuilder {
   int[] buildArray(int length);  
}

private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }

    public static void main(String[] args) {
        int arr[]=initArray(10,int[]::new);
        System.out.println(arr.length);
    }

你可能感兴趣的:(Java)