Java入门

news2024/10/6 1:34:58

文章目录

  • 数组
    • 一维数组
    • 多维数组
    • Arrays工具类
    • 数组中常见异常
  • String、StringBuilder、StringBuffer
    • String类
      • String的特性
      • String对象的创建
      • String常用方法
    • StringBuilder类
    • StringBuffer类
      • StringBuffer对象的创建
      • StringBuffer类的常用方法
    • String、StringBuffer、StringBuilder区别
  • 日期时间
    • JDK8之前日期时间API
      • java.lang.System类
      • java.util.Date类
      • java.text.SimpleDateFormat类
      • java.util.Calendar(日历)类
    • JDK8中新日期时间API
      • LocalDate、LocalTime、LocalDateTime
      • 瞬时:Instant
      • 格式化与解析日期或时间
      • 其它API
  • 异常处理
    • 异常类型
      • Error
      • Exception
      • 运行时异常
      • 编译时异常
    • 异常处理
      • try-catch-finally
      • throws(声明抛出异常)
      • throw(手动抛出异常)
      • 自定义异常类
  • 集合
    • Collection
    • Iterator
    • List
      • ArrayList
      • LinkedList
      • Vector
    • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
    • Map
      • HashMap
      • LinkedHashMap
      • TreeMap
      • HashTable
    • Properties
  • 比较器
    • 自然排序:java.lang.Comparable
    • 定制排序:java.util.Comparator
  • 枚举
    • 枚举类的使用
    • 自定义枚举类
    • 使用enum定义枚举类
    • Enum类的主要方法
  • 注解
    • 注解 (Annotation) 概述
    • 常见的Annotation示例
    • 自定义 Annotation
    • JDK 中的元注解
    • JDK8中注解的新特性
  • 泛型
    • 什么是泛型
    • 为什么引入泛型
    • 泛型用法
    • 泛型应用实例
    • 为什么要有泛型(Generic)
    • 在集合中使用泛型
    • 自定义泛型结构
      • 泛型类、泛型接口
    • 泛型方法
    • 泛型在继承上的体现
    • 通配符的使用
      • 有限制的通配符
    • 泛型应用举例
      • 泛型嵌套
  • IO流
    • File类的使用
    • File 类的使用
      • 常用构造器
      • 路径分隔符
      • 常用方法
      • File类的获取功能
      • File类的重命名功能
      • File类的判断功能
      • File类的创建功能
      • File类的删除功能
    • IO流原理及流的分类
    • 节点流
      • FileInputStream和FileOutputStream
      • FileReader和FileWriter
    • 处理流
      • BufferedInputStream和BufferedOutputSteam
      • BufferedReader和BufferedWriter
    • 转换流
      • InputStreamReader和OutputStreamWriter
  • 网络编程
  • 反射
  • Java反射机制概述
    • Java Reflection
    • 补充:动态语言 vs 静态语言
    • Java反射机制研究及应用
    • 反射相关的主要API
  • 理解Class类并获取Class的实例
    • Class 类
    • Class类的常用方法
    • 反射的应用举例
    • 获取Class类的实例(四种方法)
    • 哪些类型可以有Class对象?
  • 类的加载与ClassLoader的理解
    • 了解:类的加载过程
    • 了解:什么时候会发生类初始化?
    • 了解:ClassLoader
  • 创建运行时类的对象
    • 有了Class对象,能做什么?
  • 获取运行时类的完整结构
    • 通过反射获取运行时类的完整结构
  • 调用运行时类的指定结构
    • 1.调用指定方法
    • 2.调用指定属性
    • 关于setAccessible方法的使用
  • 反射的应用:动态代理
    • 代理设计模式的原理:
    • 动态代理步骤
    • 动态代理与AOP(Aspect Orient Programming)
  • 多线程
    • 线程的创建
      • 继承Thread类创建线程
      • 实现Runnable接口创建线程
      • 采用匿名类的方式创建线程
    • 线程的同步
      • 使用同步代码块
        • 继承Runnable接口形式同步代码块
        • 继承Thread类形式同步代码块
      • 使用同步方法
      • 使用Lock锁
    • 线程通讯
      • 实现Callable接口创建线程
  • 线程的创建和使用
    • API中创建线程的两种方式
      • 方式一:继承Thread类
      • 方式二:实现Runnable接口
      • 继承方式和实现方式的联系与区别
      • 新增方式一:实现Callable接口
      • 新增方式二:使用线程池
        • 线程池相关API
    • Thread类
    • Thread类的常用方法
    • Java的调度方法
    • 线程的优先级
    • 线程的分类
  • 线程的生命周期
  • 线程的同步
    • Synchronized
      • 方式一:同步代码块
      • 方式二:同步方法
    • Lock
    • Synchronized 与 Lock 的对比
    • 同步机制中的锁
    • 同步的范围
    • 释放锁的操作
    • 不会释放锁的操作
    • 单例设计模式之懒汉式(线程安全)
    • 线程的死锁问题
  • 线程的通信
    • wait() 与 notify() 和 notifyAll()
    • wait() 方法
    • notify()/notifyAll()
    • sleep() 和 wait()的异同
    • 经典例题:生产者/消费者问题
    • 模拟银行取钱的问题
  • Java 8 新特性
    • Lambda 表达式
  • 什么是Lamada表达式
  • 为什么引入Lamada表达式
  • Lamada语法
  • Lamada应用实例
      • 函数式接口
      • 方法的引用
      • 构造器引用
      • 数组引用
    • StreamAPI
      • 创建 Stream
      • 中间操作
      • 终止操作
    • Optional 类
    • 反射的支持增强
  • 面试
  • 问题解决
    • java:Compilation failed
    • 1. Project Structure
    • 2. Settings
    • 3. pom.xml(刚开始就设置好这里就不会出现该问题)

数组

一维数组

  • 声明

type var[] 或 type[] var

  • 初始化
    • 动态初始化:数组声明 且为数组元素分配空间赋值的操作分开进行
      int[] arr = new int[3];
      arr[0] = 3;
      arr[1] = 9;
      arr[2] = 8;
      
      String names[];
      names = new String[3];
      names[0] = “钱学森”;
      names[1] = “邓稼先”;
      names[2] = “袁隆平”;
      
    • 静态初始化:在定义数组的同时就为数组元素分配空间并赋值。
      int arr[] = new int[]{ 3, 9, 8};
      int[] arr = {3,9,8};
      String names[] = {“李四光”,“茅以升”,“华罗庚”}
      
  • 默认初始化值
    在这里插入图片描述

多维数组

  • 初始化

    • 动态初始化):
    • int[][] arr = new int[3][2];
      定义了名称为arr的二维数组
      二维数组中有3个一维数组
      每一个一维数组中有2个元素
      一维数组的名称分别为arr[0], arr[1], arr[2]
      给第一个一维数组1脚标位赋值为78写法是:arr[0][1] = 78;
    • int[][] arr = new int[3][];
      二维数组中有3个一维数组。
      每个一维数组都是默认初始化值null (注意:区别于格式1)
      可以对这个三个一维数组分别进行初始化
      arr[0] = new int[3]; arr[1] = new int[1]; arr[2] = new int[2];
      注:int[][]arr = new int[][3]; //非法
    • 静态初始化

    int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};
    定义一个名称为arr的二维数组,二维数组中有三个一维数组
    每一个一维数组中具体元素也都已初始化
    第一个一维数组 arr[0] = {3,8,2};
    第二个一维数组 arr[1] = {2,7};
    第三个一维数组 arr[2] = {9,0,1,6};
    第三个一维数组的长度表示方式:arr[2].length;
    注意特殊写法情况:int[] x,y[]; x是一维数组,y是二维数组。
    Java中多维数组不必都是规则矩阵形式

Arrays工具类

java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。

boolean equals(int[] a,int[] b) 判断两个数组是否相等。
String toString(int[] a) 输出数组信息。
void fill(int[] a,int val) 将指定值填充到数组之中。
void sort(int[] a) 对数组进行排序。
int binarySearch(int[] a,int key) 对排序后的数组进行二分法检索指定的值。

  • java.util.Arrays类的sort()方法提供了数组元素排序功能
import java.util.Arrays;
public class SortTest {
    public static void main(String[] args) {
        int [] numbers = {5,900,1,5,77,30,64,700};
        Arrays.sort(numbers);
        for(int i = 0; i < numbers.length; i++){
            System.out.println(numbers[i]);
        }
    }
}

数组中常见异常

  • 数组脚标越界异常(ArrayIndexOutOfBoundsException)
	int[] arr = new int[2];
	System.out.println(arr[2]);
	System.out.println(arr[-1]);

访问到了数组中的不存在的脚标时发生。

  • 空指针异常(NullPointerException)
	int[] arr = null;
	System.out.println(arr[0]);

arr引用没有指向实体,却在操作实体中的元素时。

String、StringBuilder、StringBuffer

  1. 存放的位置

java.lang.*;

  1. 继承的父类

java.lang.Object

  1. 实现的接口

java.io.Serializable
Comparable
CharSequence

  1. 创建与初始化

方法一

String greeting =Hello world!”;

方法二

char [] helloArray = {'h''e''l''l''o''。' };
String helloString = new String(helloArray);
  1. 获得字符串长度

public int length()
返回此字符串的长度。
长度等于字符串中Unicode代码单元的数量。

String palindrome =Dot saw I was Tod;
int len = palindrome.length();
  1. 字符串拼接

方法一
public String concat(String str)
将指定的字符串连接到此字符串的末尾。
如果参数字符串的长度为0,则String返回此对象。
否则,String返回一个对象,该对象表示一个字符序列,
该字符序列是由该String对象表示的字符序列和由参数字符串表示的字符序列的串联。

"cares".concat("s");							//返回"caress"
"to".concat("get").concat("her");				//返回"together"

方法二

"Hello," + " world" + "!";						//结果为"Hello, world!"
String string1 = "saw I was ";
System.out.println("Dot " + string1 + "Tod");	//输出Dot saw I was Tod
  1. 字符串转换为字符数组

public char [] toCharArray()
将此字符串转换为新的字符数组。
返回:新分配的字符数组,
其长度为此字符串的长度,
其内容初始化为包含此字符串表示的字符序列。

char [] helloArray = {'h''e''l''l''o''。' };
String helloString = new String(helloArray);
char[] metooArray = helloString.toCharArray();//结果为{'h','e','l','l','o','。' }

String类

字符串,使用一对""引起来表示。

String的特性

String声明为final的,不可被继承
String实现了Serializable接口:表示字符串是支持序列化的。
	  实现了Comparable接口:表示String可以比较大小
String内部定义了final char[] value用于存储字符串数据
String代表不可变的字符序列。简称:不可变性。
   体现:1.当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
		2. 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
		3. 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
字符串常量池中是不会存储相同内容的字符串的。

String对象的创建

String str = "hello";
//本质上this.value = new char[0];
String s1 = new String();
//this.value = original.value;
String s2 = new String(String original);
//this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex,int count);
  • 字符串对象是如何存储的
    String str1 = “abc”;与String str2 = new String(“abc”);的区别?
//通过字面量定义的方式:此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中。
String s1 = "javaEE";
String s2 = "javaEE";
//通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。
String s3 = new String("javaEE");
String s4 = new String("javaEE");

String s = new String(“abc”);方式创建对象,在内存中创建了几个对象?

两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:“abc”

String s1 = "javaEE";
String s2 = "hadoop";

String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;

System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false

String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop”
System.out.println(s3 == s8);//true

结论:

字符串常量存储在字符串常量池,目的是共享
字符串非常量对象存储在堆中。
常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
只要其中有一个是变量,结果就在堆中
如果拼接的结果调用intern()方法,返回值就在常量池中

  • String使用陷阱
    String s1 = “a”;

说明:在字符串常量池中创建了一个字面量为"a"的字符串。

s1 = s1 + “b”;

说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+“b”(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。

String s2 = “ab”;

说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。

String s3 = “a” + “b”;

说明:s3指向字符串常量池中已经创建的"ab"的字符串。

String s4 = s1.intern();

说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4。

String常用方法

int length():返回字符串的长度: return value.length
char charAt(int index): 返回某索引处的字符return value[index]
boolean isEmpty():判断是否是空字符串:return value.length == 0
String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
String trim():返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
int compareTo(String anotherString):比较两个字符串的大小
String substring(int beginIndex):返回一个新的字符串,它是此字符串的从
beginIndex开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始

boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf和lastIndexOf方法如果未找到都是返回-1
String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement) : 使 用 给 定 的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement) : 使 用 给 定 的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。

String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
  • String与基本数据类型转换
    字符串→基本数据类型、包装类

Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。

基本数据类型、包装类→字符串

调用String类的public String valueOf(int n)可将int型转换为字符串
相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换

  • String与字符数组转换
    字符数组→字符串

String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。

字符串→字符数组

public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
public void getChars(int srcBegin, int srcEnd, char[] dst,int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

  • String与字节数组转换
    字节数组→字符串

String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。

字符串→字节数组

public byte[] getBytes() :使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

String str = "中";
System.out.println(str.getBytes("ISO8859-1").length);// -128~127
System.out.println(str.getBytes("GBK").length);
System.out.println(str.getBytes("UTF-8").length);
System.out.println(new String(str.getBytes("ISO8859-1"),"ISO8859-1"));// 乱码,表示不了中文
System.out.println(new String(str.getBytes("GBK"), "GBK"));
System.out.println(new String(str.getBytes("UTF-8"), "UTF-8"));

StringBuilder类

StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样

StringBuffer类

java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。
很多方法与String相同。
作为参数传递时,方法内部可以改变值。
count记录有效字符的个数。
value没有final声明,value可以不断扩容。

StringBuffer对象的创建

StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:
StringBuffer():初始容量为16的字符串缓冲区
StringBuffer(int size):构造指定容量的字符串缓冲区
StringBuffer(String str):将内容初始化为指定字符串内容

StringBuffer类的常用方法

StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
StringBuffer delete(int start,int end):删除指定位置的内容
StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
StringBuffer insert(int offset, xxx):在指定位置插入xxx
StringBuffer reverse() :把当前字符序列逆转
当append和insert时,如果原来value数组长度不够,可扩容。
如上这些方法支持方法链操作。

public int indexOf(String str)
public String substring(int start,int end)
public int length()
public char charAt(int n )
public void setCharAt(int n ,char ch)

String、StringBuffer、StringBuilder区别

String:不可变的字符序列;底层使用char[]存储
StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]存储
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储

源码分析:

String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};

StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组。
System.out.println(sb1.length());//
sb1.append('a');//value[0] = 'a';
sb1.append('b');//value[1] = 'b';

StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char["abc".length() + 16];

//问题1. System.out.println(sb2.length());//3
//问题2. 扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。
         默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中。

        指导意义:开发中建议大家使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)

日期时间

JDK8之前日期时间API

java.lang.System类

System类提供的public static long currentTimeMillis()用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。
此方法适于计算时间差。

计算世界时间的主要标准有:

UTC(Coordinated Universal Time)
GMT(Greenwich Mean Time)
CST(Central Standard Time)

java.util.Date类

表示特定的瞬间,精确到毫秒

构造器

Date():使用无参构造器创建的对象可以获取本地当前时间。
Date(long date)

常用方法

getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
toString():把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 
	  其中: dow 是一周中的某一天 (Sun, Mon, Tue,Wed, Thu, Fri, Sat),zzz是时间标准。
其它很多方法都过时了。

Date类的API不易于国际化,大部分被废弃了

java.text.SimpleDateFormat类

java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
它允许进行
格式化:日期→文本
解析:文本→日期

格式化:

SimpleDateFormat() :默认的模式和语言环境创建对象
public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用:
public String format(Date date):方法格式化时间对象date

解析:

public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。
@Test
public void testSimpleDateFormat() throws ParseException {
	//实例化SimpleDateFormat:使用默认的构造器
	SimpleDateFormat sdf = new SimpleDateFormat();
	//格式化:日期→字符串
	Date date = new Date();
	String format = sdf.format(date);
	System.out.println(format);
	//解析:格式化的逆过程,字符串→日期
	String str = "19-12-18 上午11:43";
	Date date1 = sdf.parse(str);
	System.out.println(date1);
	
	//按照指定的方式格式化和解析:调用带参的构造器
	SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
	//格式化
	String format1 = sdf1.format(date);
	System.out.println(format1);//2019-02-18 11:48:27
	//解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现),否则,抛异常
	Date date2 = sdf1.parse("2020-02-18 11:48:27");
	System.out.println(date2);
}

java.util.Calendar(日历)类

Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能。

获取Calendar实例的方法

使用Calendar.getInstance()方法
调用它的子类GregorianCalendar的构造器。
一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想要的时间信息。
比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND

public void set(int field,int value)
public void add(int field,int amount)
public final Date getTime()
public final void setTime(Date date)

注意:
获取月份时:一月是0,二月是1,以此类推,12月是11
获取星期时:周日是1,周二是2 , 。。。。周六是7

@Test
public void testCalendar(){
    //1.实例化
    //方式一:创建其子类(GregorianCalendar)的对象
    //方式二:调用其静态方法getInstance()
    Calendar calendar = Calendar.getInstance();
    //2.常用方法
    //get()
    int days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);
    System.out.println(calendar.get(Calendar.DAY_OF_YEAR));
    
    //set()
    //calendar可变性
    calendar.set(Calendar.DAY_OF_MONTH,22);
    days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);
    
    //add()
    calendar.add(Calendar.DAY_OF_MONTH,-3);
    days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);

    //getTime():日历类---> Date
    Date date = calendar.getTime();
    System.out.println(date);

    //setTime():Date ---> 日历类
    Date date1 = new Date();
    calendar.setTime(date1);
    days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);
}

JDK8中新日期时间API

  • 新日期时间API出现的背景

如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
可变性:像日期和时间这样的类应该是不可变的。
偏移性:Date中的年份是从1900开始的,而月份都从0开始。
格式化:格式化只对Date有用,Calendar则不行。
此外,它们也不是线程安全的;不能处理闰秒等。
总结:对日期和时间的操作一直是Java程序员最痛苦的地方之一。

  • 新时间日期API

第三次引入的API是成功的,并且Java 8中引入的java.time API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。
Java 8 吸收了 Joda-Time 的精华,以一个新的开始为 Java 创建优秀的 API。
新的 java.time 中包含了所有关于本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 toInstant() 方法,用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理。

java.time – 包含值对象的基础包
java.time.chrono – 提供对不同的日历系统的访问
java.time.format – 格式化和解析时间和日期
java.time.temporal – 包括底层框架和扩展特性
java.time.zone – 包含时区支持的类

说明:大多数开发者只会用到基础包和format包,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概将只会用到其中的三分之一。

LocalDate、LocalTime、LocalDateTime

LocalDate、LocalTime、LocalDateTime 类是其中较重要的几个类,它们的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。
它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。
LocalTime表示一个时间,而不是日期。
LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。
注:ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法,也就是公历。

now() / * now(ZoneId zone) 静态方法,根据当前时间创建对象/指定时区的对象of() 静态方法,根据指定日期/时间创建对象

getDayOfMonth()/getDayOfYear() 获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek() 获得星期几(返回一个 DayOfWeek 枚举值)
getMonth() 获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear() 获得月份(1-12) /获得年份
getHour()/getMinute()/getSecond() 获得当前对象对应的小时、分钟、秒

withDayOfMonth()/withDayOfYear()/withMonth()/withYear() 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
plusDays(), plusWeeks(),plusMonths(), plusYears(),plusHours() 向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours() 从当前对象减去几月、几周、几天、几年、几小时
@Test
public void test(){
    //now():获取当前的日期、时间、日期+时间
    LocalDate localDate = LocalDate.now();
    LocalTime localTime = LocalTime.now();
    LocalDateTime localDateTime = LocalDateTime.now();

    //of():设置指定的年、月、日、时、分、秒。没有偏移量
    LocalDateTime localDateTime1 = LocalDateTime.of(2020, 10, 6, 13, 23, 43);

    //getXxx():获取相关的属性
    System.out.println(localDateTime.getDayOfMonth());
    System.out.println(localDateTime.getDayOfWeek());
    System.out.println(localDateTime.getMonth());
    System.out.println(localDateTime.getMonthValue());
    System.out.println(localDateTime.getMinute());

    //体现不可变性
    //withXxx():设置相关的属性
    LocalDate localDate1 = localDate.withDayOfMonth(22);
    System.out.println(localDate);
    System.out.println(localDate1);

    LocalDateTime localDateTime2 = localDateTime.withHour(4);
    System.out.println(localDateTime);
    System.out.println(localDateTime2);

    //不可变性
    LocalDateTime localDateTime3 = localDateTime.plusMonths(3);
    System.out.println(localDateTime);
    System.out.println(localDateTime3);

    LocalDateTime localDateTime4 = localDateTime.minusDays(6);
    System.out.println(localDateTime);
    System.out.println(localDateTime4);
}

瞬时:Instant

Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。
在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样的,在Java中,也是从1970年开始,但以毫秒为单位。
java.time包通过值类型Instant提供机器视图,不提供处理人类意义上的时间单位。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。
概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级

now() 静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli) 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset) 结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli() 返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳
时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
@Test
public void test2(){
    //now():获取本初子午线对应的标准时间
    Instant instant = Instant.now();
    //添加时间的偏移量
    OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));

    //toEpochMilli():获取自1970年1月1日0时0分0秒(UTC)开始的毫秒数  ---> Date类的getTime()
    long milli = instant.toEpochMilli();

    //ofEpochMilli():通过给定的毫秒数,获取Instant实例  -->Date(long millis)
    Instant instant1 = Instant.ofEpochMilli(1550475314878L);
}

格式化与解析日期或时间

java.time.format.DateTimeFormatter 类:
该类提供了三种格式化方法:

预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

ofPattern(String pattern) 静态方法 , 返 回 一 个 指 定 字 符 串 格 式 的DateTimeFormatter
format(TemporalAccessor t) 格式化一个日期、时间,返回字符串
parse(CharSequence text) 将指定格式的字符序列解析为一个日期、时间
@Test
public void test3(){
//方式一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
    DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    //格式化:日期-->字符串
    LocalDateTime localDateTime = LocalDateTime.now();
    String str1 = formatter.format(localDateTime);
    System.out.println(localDateTime);
    System.out.println(str1);//2019-02-18T15:42:18.797

    //解析:字符串 -->日期
    TemporalAccessor parse = formatter.parse("2019-02-18T15:42:18.797");
    System.out.println(parse);

//方式二:本地化相关的格式。如:ofLocalizedDateTime()
//FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime
    DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
    //格式化
    String str2 = formatter1.format(localDateTime);
    System.out.println(str2);//2019年2月18日 下午03时47分16秒


//本地化相关的格式。如:ofLocalizedDate()
//FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate
    DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
    //格式化
    String str3 = formatter2.format(LocalDate.now());
    System.out.println(str3);//2019-2-18


//重点: 方式三:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
    DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    //格式化
    String str4 = formatter3.format(LocalDateTime.now());
    System.out.println(str4);//2019-02-18 03:52:09

    //解析
    TemporalAccessor accessor = formatter3.parse("2019-02-18 03:52:09");
    System.out.println(accessor);
}

其它API

ZoneId:该类中包含了所有的时区信息,一个时区的ID,如 Europe/Paris
ZonedDateTime:一个在ISO-8601日历系统时区的日期时间,如 2007-12-03T10:15:30+01:00 Europe/Paris。
	其中每个时区都对应着ID,地区ID都为“{区域}/{城市}”的格式,例如:Asia/Shanghai等
Clock:使用时区提供对当前即时、日期和时间的访问的时钟。
持续时间:Duration,用于计算两个“时间”间隔
日期间隔:Period,用于计算两个“日期”间隔
TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作。
TemporalAdjusters : 该类通过静态方法(firstDayOfXxx()/lastDayOfXxx()/nextXxx())提供了大量的常用TemporalAdjuster 的实现。

异常处理

异常类型

在这里插入图片描述

Error

Java虚拟机无法解决的严重问题。
如:JVM系统内部错误、资源耗尽等严重情况。
比如:StackOverflowError和OOM。一般不编写针对性的代码进行处理。

Exception

其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
空指针访问
试图读取不存在的文件
网络连接中断
数组角标越界

运行时异常

是指编译器不要求强制处置的异常。
一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。
java.lang.RuntimeException类及它的子类都是运行时异常。
对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对
程序的可读性和运行效率产生影响。

编译时异常

是指编译器要求必须处置的异常。
即程序在运行时由于外界因素造成的一般性异常。
编译器要求Java程序必须捕获或声明所有编译时异常。
对于这类异常,如果程序不处理,可能会带来意想不到的结果。

异常处理

在这里插入图片描述

try-catch-finally

try
捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。
catch (Exceptiontype e)
在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
捕获异常的有关信息:
与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。

  • getMessage() 获取异常信息,返回字符串
  • printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。

finally
捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。
不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。
finally语句和catch语句是任选的

throws(声明抛出异常)

如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。
声明抛出异常举例:

public void readFile(String file) throws FileNotFoundException {
	……
	// 读文件的操作可能产生FileNotFoundException类型的异常
	FileInputStream fis = new FileInputStream(file);
	..……
}

throw(手动抛出异常)

首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。

IOException e = new IOException();
throw e;

可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将
会产生语法错误:
throw new String("want to throw");

自定义异常类

一般地,用户自定义异常类都是RuntimeException的子类。
自定义异常类通常需要编写几个重载的构造器
自定义异常需要提供serialVersionUID
自定义的异常通过throw抛出。
自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。

用户自定义异常类MyException,用于描述数据取值范围错误信息。
用户自己的异常类必须继承现有的异常类。
class MyException extends Exception {
	static final long serialVersionUID = 13465653435L;
	private int idnumber;
	public MyException(String message, int id) {
		super(message);
		this.idnumber = id;
	}
	public int getId() {
		return idnumber;
	}
}

public class MyExpTest {
	public void regist(int num) throws MyException {
		if (num < 0)
			throw new MyException("人数为负值,不合理", 3);
		else
			System.out.println("登记人数" + num);
	}
	public void manager() {
		try {
			regist(100);
		} catch (MyException e) {
			System.out.print("登记失败,出错种类" + e.getId());
		}
		System.out.print("本次登记操作结束");
	}
	public static void main(String args[]) {
		MyExpTest t = new MyExpTest();
		t.manager();
	}
}

集合

Java 集合可分为 Collection 和 Map 两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合

  • List:元素有序、可重复的集合
  • Set:元素无序、不可重复的集合

Map接口:双列数据,保存具有映射关系“key-value对”的集合

Collection

在这里插入图片描述

是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
boolean equals(Object obj):集合是否相等
Object[] toArray():转成对象数组
iterator():返回迭代器对象,用于集合遍历

Iterator

Iterator iter = coll.iterator();//回到起点
//hasNext():判断是否还有下一个元素
while(iter.hasNext()){
	//next():①指针下移 ②将下移以后集合位置上的元素返回
	Object obj = iter.next();
	if(obj.equals("Tom")){
		iter.remove();
	}
}

Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。
遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
遍历集合的底层调用Iterator完成操作。
foreach还可以用来遍历数组。

List

ArrayList和LinkedList的异同

二者都线程不安全,相对线程安全的Vector,执行效率高。
此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

ArrayList和Vector的区别

Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。
正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。
Vector还有一个子类Stack。

ArrayList

LinkedList

Vector

Set

HashSet

LinkedHashSet

TreeSet

Map

在这里插入图片描述

Map中的key:
无序的、不可重复的,使用Set存储所有的key→key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:
无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals()
一个键值对:key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry

Map:双列数据,存储key-value对的数据
	HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
		LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
        	在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
        	对于频繁的遍历操作,此类执行效率高于HashMap。
	TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。
		此时考虑key的自然排序或定制排序
    	底层使用红黑树
	Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
		Properties:常用来处理配置文件。key和value都是String类型

HashMap

HashMap的底层实现原理?

1.JDK7中

HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。

map.put(key1,value1):
  首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
  如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
  如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
  	如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
  	如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
        如果equals()返回false:此时key1-value1添加成功。----情况3
        如果equals()返回true:使用value1替换value2。
   补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

  在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

2.JDK8中

  jdk8 相较于jdk7在底层实现方面的不同:
  1. new HashMap():底层没有创建一个长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
     4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
     4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

  DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
  threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
  TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
  MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

LinkedHashMap

TreeMap

HashTable

Properties

比较器

在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。
Java实现对象排序的方式有两种:
自然排序:java.lang.Comparable
定制排序:java.util.Comparator

自然排序:java.lang.Comparable

Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
如果当前对象this大于形参对象obj,则返回正整数,
如果当前对象this小于形参对象obj,则返回负整数,
如果当前对象this等于形参对象obj,则返回零。
实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals一致。建议(虽然不是必需的)最好使自然排序与 equals 一致。

Comparable 的典型实现:(默认都是从小到大排列的)
String:按照字符串中字符的Unicode值进行比较
Character:按照字符的Unicode值来进行比较
数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
Boolean:true 对应的包装类实例大于 false 对应的包装类实例
Date、Time等:后面的日期时间比前面的日期时间大

class Goods implements Comparable {
	private String name;
	private double price;
	//按照价格,比较商品的大小
	@Override
	public int compareTo(Object o) {
		if(o instanceof Goods) {
			Goods other = (Goods) o;
			if (this.price > other.price) {
				return 1;
			} else if (this.price < other.price) {
				return -1;
			}
				return 0;
			}
		throw new RuntimeException("输入的数据类型不一致");
	}
	//构造器、getter、setter、toString()方法略
}

public class ComparableTest{
	public static void main(String[] args) {
		Goods[] all = new Goods[4];
		all[0] = new Goods("《红楼梦》", 100);
		all[1] = new Goods("《西游记》", 80);
		all[2] = new Goods("《三国演义》", 140);
		all[3] = new Goods("《水浒传》", 120);
		Arrays.sort(all);
		System.out.println(Arrays.toString(all));
	}
}

定制排序:java.util.Comparator

当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排序的比较。
重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。
还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。

@Test
public void test(){
    String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
    Arrays.sort(arr,new Comparator(){
        //按照字符串从大到小的顺序排列
        @Override
        public int compare(Object o1, Object o2) {
            if(o1 instanceof String && o2 instanceof  String){
                String s1 = (String) o1;
                String s2 = (String) o2;
                return -s1.compareTo(s2);
            }
            throw new RuntimeException("输入的数据类型不一致");
        }
    });
    System.out.println(Arrays.toString(arr));
}

枚举

枚举类的使用

类的对象只有有限个,确定的。举例如下:
星期:Monday(星期一)、…、Sunday(星期天)
性别:Man(男)、Woman(女)
季节:Spring(春节)…Winter(冬天)
支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
就职状态:Busy、Free、Vocation、Dimission
订单状态:Nonpayment(未付款)、Paid(已付款)、Delivered(已发货)、
Return(退货)、Checked(已确认)Fulfilled(已配货)、
线程状态:创建、就绪、运行、阻塞、死亡
当需要定义一组常量时,强烈建议使用枚举类

枚举类的实现

JDK1.5之前需要自定义枚举类
JDK 1.5 新增的 enum 关键字用于定义枚举类
若枚举只有一个对象, 则可以作为一种单例模式的实现方式。

枚举类的属性

枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰
枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数

自定义枚举类

私有化类的构造器,保证不能在类的外部创建其对象
在类的内部创建枚举类的实例。声明为:public static final
对象如果有实例变量,应该声明为private final,并在构造器中初始化

//自定义枚举类
class Season{
    //1.声明Season对象的属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

    //2.私有化类的构造器,并给对象属性赋值
    private Season(String seasonName,String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }

    //3.提供当前枚举类的多个对象:public static final的
    public static final Season SPRING = new Season("春天","春暖花开");
    public static final Season SUMMER = new Season("夏天","夏日炎炎");
    public static final Season AUTUMN = new Season("秋天","秋高气爽");
    public static final Season WINTER = new Season("冬天","冰天雪地");

    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }
    //4.其他诉求1:提供toString()
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

使用enum定义枚举类

使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类
枚举类的构造器只能使用 private 权限修饰符
枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾)。列出的实例系统会自动添加 public static final 修饰
必须在枚举类的第一行声明枚举类对象
JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。

//使用enum关键字定义的枚举类实现接口的情况
//情况一:实现接口,在enum类中实现抽象方法
//情况二:让枚举类的对象分别实现接口中的抽象方法
interface Info{
    void show();
}
//使用enum关键字枚举类
enum Season implements Info {
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天","春暖花开"){
        @Override
        public void show() {
            System.out.println("春天在哪里?");
        }
    },
    SUMMER("夏天","夏日炎炎"){
        @Override
        public void show() {
            System.out.println("宁夏");
        }
    },
    AUTUMN("秋天","秋高气爽"){
        @Override
        public void show() {
            System.out.println("秋天不回来");
        }
    },
    WINTER("冬天","冰天雪地"){
        @Override
        public void show() {
            System.out.println("大约在冬季");
        }
    };

    //2.声明Season对象的属性:private final修饰
    private final String seasonName;
    private final String seasonDesc;

    //3.私有化类的构造器,并给对象属性赋值
    private Season(String seasonName,String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

Enum类的主要方法

values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
toString():返回当前枚举类对象常量的名称

和普通 Java 类一样,枚举类可以实现一个或多个接口
若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法

注解

主要内容:
注解(Annotation)概述
常见的Annotation示例
自定义Annotation
JDK中的元注解
利用反射获取注解信息(在反射部分涉及)
JDK 8中注解的新特性

注解 (Annotation) 概述

从 JDK 5.0 开始, Java 增加了对元数据(MetaData) 的支持, 也就是Annotation(注解)
Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
Annotation 可以像修饰符一样被使用, 可用于修饰包,类, 构造器, 方法, 成员变量, 参数, 局部变量的声明, 这些信息被保存在 Annotation的 “name=value” 对中。

在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。
未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式。

常见的Annotation示例

使用 Annotation 时要在其前面增加 @ 符号, 并把该 Annotation 当成
一个修饰符使用。用于修饰它支持的程序元素

  • 生成文档相关的注解

@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
其中
@param @return 和 @exception 这三个标记都是只用于方法的。
@param的格式要求:@param 形参名 形参类型 形参说明
@return 的格式要求:@return 返回值类型 返回值说明
@exception的格式要求:@exception 异常类型 异常说明
@param和@exception可以并列多个

package com.annotation.javadoc;
/**
* @author shkstart
* @version 1.0
* @see Math.java
*/
public class JavadocTest {
	/**
	* 程序的主方法,程序的入口
	* @param args String[] 命令行参数
	*/
	public static void main(String[] args) {
		
	}
	/**
	* 求圆面积的方法
	* @param radius double 半径值
	* @return double 圆的面积
	*/
	public static double getArea(double radius){
		return Math.PI * radius * radius;
	}
}
  • 在编译时进行格式检查(JDK内置的三个基本注解)

@Override: 限定重写父类方法, 该注解只能用于方法
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
@SuppressWarnings: 抑制编译器警告

package com.annotation.javadoc;
public class AnnotationTest{
	public static void main(String[] args) {
		@SuppressWarnings("unused")
		int a = 10;
	}
	@Deprecated
	public void print(){
		System.out.println("过时的方法");
	}
	@Override
	public String toString() {
		return "重写的toString方法()";
	}
}
  • 跟踪代码依赖性,实现替代配置文件功能
    Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 

	}
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	} 
}
<servlet>
	<servlet-name>LoginServlet</servlet-name>
	<servlet-class>com.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>LoginServlet</servlet-name>
	<url-pattern>/login</url-pattern>
</servlet-mapping>

spring框架中关于“事务”的管理

@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3)
public void buyBook(String username, String isbn) {
	//1.查询书的单价
	int price = bookShopDao.findBookPriceByIsbn(isbn);
	//2. 更新库存
	bookShopDao.updateBookStock(isbn);
	//3. 更新用户的余额
	bookShopDao.updateUserAccount(username, price);
}
<!-- 配置事务属性 -->
<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
<tx:attributes>
<!-- 配置每个方法使用的事务属性 -->
<tx:method name="buyBook" propagation="REQUIRES_NEW"
isolation="READ_COMMITTED" read-only="false" timeout="3" />
</tx:attributes>
</tx:advice>

自定义 Annotation

定义新的 Annotation 类型使用 @interface 关键字
自定义注解自动继承了java.lang.annotation.Annotation接口
Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
可以在定义 Annotation 的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
如果只有一个参数成员,建议使用参数名为value
如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”没有成员定义的 Annotation 称为标记; 包含成员变量的 Annotation 称为元数据 Annotation
注意:自定义注解必须配上注解的信息处理流程才有意义。

@MyAnnotation(value="尚硅谷")
public class MyAnnotationTest {
	public static void main(String[] args) {
		Class clazz = MyAnnotationTest.class;
		Annotation a = clazz.getAnnotation(MyAnnotation.class);
		MyAnnotation m = (MyAnnotation) a;
		String info = m.value();
		System.out.println(info);
	}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation{
	String value() default "auguigu";
}

JDK 中的元注解

JDK 的元 Annotation 用于修饰其他 Annotation 定义
JDK5.0提供了4个标准的meta-annotation类型,分别是:
Retention、Target、Documented、Inherited
元数据的理解:
String name = “atguigu”;

@Retention: 只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 的生命周期, @Rentention 包含一个 RetentionPolicy 类型的成员变量, 使用@Rentention 时必须为该 value 成员变量指定值:
	RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
	RetentionPolicy.CLASS:在class文件中有效(即class保留) , 当运行 Java 程序时, JVM不会保留注解。 这是默认值
	RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时, JVM 会保留注释。程序可以通过反射获取该注释。
public enum RetentionPolicy{
	SOURCE,
	CLASS,
	RUNTIME
}
@Retention(RetentionPolicy.SOURCE)
@interface MyAnnotation1{
}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{
}
@Target: 用于修饰 Annotation 定义, 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 
@Target 也包含一个名为 value 的成员变量。

在这里插入图片描述

@Documented: 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。
	定义为Documented的注解必须设置Retention值为RUNTIME。
	
@Inherited: 被它修饰的 Annotation 将具有继承性。如果某个类使用了被@Inherited 修饰的 Annotation, 则其子类将自动具有该注解。
	比如:如果把标有@Inherited注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解
	实际应用中,使用较少
  • 利用反射获取注解信息
    JDK 5.0 在 java.lang.reflect 包下新增了 AnnotatedElement 接口, 该接口代表程序中可以接受注解的程序元素
    当一个 Annotation 类型被定义为运行时 Annotation 后, 该注解才是运行时可见, 当 class 文件被载入时保存在 class 文件中的 Annotation 才会被虚拟机读取
    程序可以调用 AnnotatedElement对象的如下方法来访问 Annotation 信息

JDK8中注解的新特性

Java 8对注解处理提供了两点改进:可重复的注解及可用于类型的注解。
此外,反射也得到了加强,在Java8中能够得到方法参数的名称。这会简化标注在方法参数上的注解。

可重复注解示例:
在这里插入图片描述

类型注解:
JDK1.8之后,关于元注解@Target的参数类型ElementType枚举值多了两个:TYPE_PARAMETER,TYPE_USE。
在Java 8之前,注解只能是在声明的地方所使用,Java8开始,注解可以应用在任何地方。
ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。

public class TestTypeDefine<@TypeDefine() U> {
	private U u;
	public <@TypeDefine() T> void test(T t){
		
	}
}
@Target({ElementType.TYPE_PARAMETER})
@interface TypeDefine{
	
}
@MyAnnotation
public class AnnotationTest<U> {
	@MyAnnotation
	private String name;
	public static void main(String[] args) {
		AnnotationTest<@MyAnnotation String> t = null;
		int a = (@MyAnnotation int) 2L;
		@MyAnnotation
		int b = 10;
	}
	public static <@MyAnnotation T> void method(T t) {
		
	}
	public static void test(@MyAnnotation String arg) throws @MyAnnotation Exception {
		
	}
}
@Target(ElementType.TYPE_USE)
@interface MyAnnotation {
	
}

泛型

什么是泛型

基本含义:

泛型是程序设计语言的一种特性。
允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。
各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。
将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。
泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念。

Java中:

为什么引入泛型

泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
例如:我们并不希望为聚集String和File对象分别设计不同的类。实际上,也不需要这样做,因为一个ArrayList类可以聚集任何类型的对象。
引入泛型前:

泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:
问题:当获取一个值时必须进行强制类型转换。
这里没有错误检查。可以向数组列表中添加任何类的对象。
引入后:
类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型:[插图]这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是String对象。

泛型用法

泛型应用实例

为什么要有泛型(Generic)

泛型:标签
举例:
中药店,每个抽屉外面贴着标签
超市购物架上很多瓶子,每个瓶子装的是什么,有标签泛型的设计背景
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection,List,ArrayList 这个就是类型参数,即泛型。

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List,这表明该List只能保存字符串类型的对象。
JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

那么为什么要有泛型呢,直接Object不是也可以存储数据吗?

  1. 解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
  2. 解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药
    品都要辨别。

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。

在集合中使用泛型

① 集合接口或集合类在jdk5.0时都修改为带泛型的结构。
② 在实例化集合类时,可以指明具体的泛型类型
③ 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。
比如:add(E e) —>实例化以后:add(Integer e)
④ 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换
⑤ 如果实例化时,没有指明泛型的类型。默认类型为java.lang.Object类型。

ArrayList<Integer> list = new ArrayList<>();//类型推断
list.add(78);
list.add(88);
list.add(77);
list.add(66);
//遍历方式一:
//for(Integer i : list){
	//不需要强转
	//System.out.println(i);
//}
//遍历方式二:
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
	System.out.println(iterator.next());
}

Map<String,Integer> map = new HashMap<String,Integer>();
map.put("Tom1",34);
map.put("Tom2",44);
map.put("Tom3",33);
map.put("Tom4",32);
//添加失败
//map.put(33, "Tom");
Set<Entry<String,Integer>> entrySet = map.entrySet();
Iterator<Entry<String,Integer>> iterator = entrySet.iterator();
while(iterator.hasNext()){
	Entry<String,Integer> entry = iterator.next();
	System.out.println(entry.getKey() + "--->" + entry.getValue());
}

自定义泛型结构

泛型类、泛型接口

1.泛型的声明
interface List 和 class GenTest<K,V>
其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。
常用T表示,是Type的缩写。
2.泛型的实例化:
一定要在类名后面指定类型参数的值(类型)。如:
List strList = new ArrayList();
Iterator iterator = customers.iterator();
T只能是类,不能用基本数据类型填充。但可以使用包装类填充
把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想

Comparable c = new Date();
System.out.println(c.compareTo(“red”));
Comparable c = new Date();
System.out.println(c.compareTo(“red”));

体会:使用泛型的主要优点是能够在编译时而不是在运行时检测错误。

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
  2. 泛型类的构造器如下:public GenericClass(){}
    而下面是错误的:public GenericClass<E>(){}
  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
  4. 泛型不同的引用不能相互赋值。
    尽管在编译时ArrayList<String>ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  7. jdk1.7,泛型的简化操作:ArrayList flist = new ArrayList<>();
  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
class GenericTest {
	public static void main(String[] args) {
		// 1、使用时:类似于Object,不等同于Object
		ArrayList list = new ArrayList();
		// list.add(new Date());//有风险
		list.add("hello");
		test(list);// 泛型擦除,编译不会类型检查
		// ArrayList<Object> list2 = new ArrayList<Object>();
		// test(list2);//一旦指定Object,编译会类型检查,必须按照Object处理
	}
	public static void test(ArrayList<String> list) {
		String str = "";
		for (String s : list) {
			str += s + ",";
		}
		System.out.println("元素:" + str);
	}
}
  1. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
  2. 异常类不能是泛型的
  3. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
    参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
  4. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
    子类不保留父类的泛型:按需实现
    没有类型 擦除
    具体类型
    子类保留父类的泛型:泛型子类
    全部保留
    部分保留
    结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
}
class Person<T> {
	// 使用T类型定义变量
	private T info;
	// 使用T类型定义一般方法
	public T getInfo() {
	return info;
	}
	public void setInfo(T info) {
	this.info = info;
	}
	// 使用T类型定义构造器
	public Person() {
	}
	public Person(T info) {
	this.info = info;
	}
	// static的方法中不能声明泛型
	//public static void show(T t) {
	//
	//}
	// 不能在try-catch中使用泛型定义
	//public void test() {
	//try {
	//
	//} catch (MyException<T> ex) {
	//
	//}
	//}
}

泛型方法

方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
泛型方法声明泛型时也可以指定上限(在12.5中讲)

public class DAO {
	public <E> E get(int id, E e) {
		E result = null;
		return result;
	}
}
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
	for (T o : a) {
		c.add(o);
	}
}
public static void main(String[] args) {
	Object[] ao = new Object[100];
	Collection<Object> co = new ArrayList<Object>();
	fromArrayToCollection(ao, co);
	String[] sa = new String[20];
	Collection<String> cs = new ArrayList<>();
	fromArrayToCollection(sa, cs);
	Collection<Double> cd = new ArrayList<>();
	// 下面代码中T是Double类,但sa是String类型,编译错误。
	// fromArrayToCollection(sa, cd);
	// 下面代码中T是Object类型,sa是String类型,可以赋值成功。
	fromArrayToCollection(sa, co);
}
class Creature{}
class Person extends Creature{}
class Man extends Person{}
class PersonTest {
	public static <T extends Person> void test(T t){
		System.out.println(t);
	}
	public static void main(String[] args) {
		test(new Person());
		test(new Man());
		//The method test(T) in the type PersonTest is not
		//applicable for the arguments (Creature)
		test(new Creature());
	}
}

泛型在继承上的体现

public void printCollection(Collection c) {
	Iterator i = c.iterator();
	for (int k = 0; k < c.size(); k++) {
		System.out.println(i.next());
	}
}

public void printCollection(Collection<Object> c) {
	for (Object e : c) {
		System.out.println(e);
	}
}

如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型!
比如:String是Object的子类,但是List<String >并不是List<Object>的子类。

public void testGenericAndSubClass() {
	Person[] persons = null;
	Man[] mans = null;
	// 而 Person[] 是 Man[] 的父类.
	persons = mans;
	Person p = mans[0];
	// 在泛型的集合上
	List<Person> personList = null;
	List<Man> manList = null;
	// personList = manList;(报错)
}

通配符的使用

  1. 使用类型通配符:?
    比如:List<?> ,Map<?,?>
    List<?>是List<String>List<Object>等各种泛型List的父类。
  2. 读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
  3. 写入list中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。
    唯一的例外是null,它是所有类型的成员。
    将任意元素加入到其中不是类型安全的:
    Collection<?> c = new ArrayList<String>();
    c.add(new Object()); // 编译时错误
    因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
    唯一的例外的是null,它是所有类型的成员。
    另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。
public static void main(String[] args) {
	List<?> list = null;
	list = new ArrayList<String>();
	list = new ArrayList<Double>();
	// list.add(3);//编译不通过
	list.add(null);
	List<String> l1 = new ArrayList<String>();
	List<Integer> l2 = new ArrayList<Integer>();
	l1.add("尚硅谷");
	l2.add(15);
	read(l1);
	read(l2);
}
public static void read(List<?> list) {
	for (Object o : list) {
		System.out.println(o);
	}
}
//注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
}
//注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
}
//注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();
<?>

允许所有泛型的引用调用
通配符指定上限
上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
通配符指定下限
下限super:使用时指定的类型不能小于操作的类,即>=
举例:
<? extends Number> (无穷小 , Number]
只允许泛型为Number及Number子类的引用调用
<? super Number> [Number , 无穷大)
只允许泛型为Number及Number父类的引用调用
<? extends Comparable>
只允许泛型为实现Comparable接口的实现类的引用调用

有限制的通配符

public static void printCollection3(Collection<? extends Person> coll) {
	//Iterator只能用Iterator<?>或Iterator<? extends Person>.why?
	Iterator<?> iterator = coll.iterator();
	while (iterator.hasNext()) {
		System.out.println(iterator.next());
	}
}
public static void printCollection4(Collection<? super Person> coll) {
	//Iterator只能用Iterator<?>或Iterator<? super Person>.why?
	Iterator<?> iterator = coll.iterator();
	while (iterator.hasNext()) {
		System.out.println(iterator.next());
	}
}

泛型应用举例

泛型嵌套

public static void main(String[] args) {
	HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>();
	ArrayList<Citizen> list = new ArrayList<Citizen>();
	list.add(new Citizen("刘恺威"));
	list.add(new Citizen("杨幂"));
	list.add(new Citizen("小糯米"));
	map.put("刘恺威", list);
	Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet();
	Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator();
	while (iterator.hasNext()) {
		Entry<String, ArrayList<Citizen>> entry = iterator.next();
		String key = entry.getKey();
		ArrayList<Citizen> value = entry.getValue();
		System.out.println("户主:" + key);
		System.out.println("家庭成员:" + value);
	}
}

用户在设计类的时候往往会使用类的关联关系,例如,一个人中可以定义一个信息的属性,但是一个人可能有各种各样的信息(如联系方式、基本信息等),所以此信息属性的类型就可以通过泛型进行声明,然后只要设计相应的信息类即可。

IO流

File类的使用

  1. File类的理解

File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)。
File类声明在 java.io 包下:文件和文件路径的抽象表示形式,与平台无关。
File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java程序中的一个 File 对象,可能没有一个真实存在的文件或目录。
后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。

  1. File的实例化
    2.1 常用构造器

File(String filePath)
File(String parentPath,String childPath)
File(File parentFile,String childPath)

代码示例:
@Test
public void test1() {
//构造器1
File file1 = new File(“hello.txt”);
File file2 = new File(“E:\workspace_idea\JavaSenic\IO\hello.txt”);
System.out.println(file1);
System.out.println(file2);

//构造器2
File file3 = new File("E:\\workspace_idea\\JavaSenior", "hello.txt");
System.out.println(file3);

//构造器3
File file4 = new File(file3, "hi.txt");
System.out.println(file4);

}

复制代码
2.2 路径分类

相对路径:相较于某个路径下,指明的路径。
绝对路径:包含盘符在内的文件或文件目录的路径。

说明:

IDEA中:

如果使用JUnit中的单元测试方法测试,相对路径即为当前Module下。
如果使用main()测试,相对路径即为当前的Project下。

Eclipse中:

不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。

2.3 路径分隔符

windows和DOS系统默认使用 \ 来表示

UNIX和URL使用 / 来表示

Java程序支持跨平台运行,因此路径分隔符要慎用。

为了解决这个隐患,File类提供了一个常量: public static final String separator。根据操作系统,动态的提供分隔符。
举例:
//windows和DOS系统
File file1 = new File(“E:\io\test.txt”);
//UNIX和URL
File file = new File(“E:/io/test.txt”);
//java提供的常量
File file = new File(“E:”+File.separator+“io”+File.separator+“test.txt”);

复制代码

  1. File类的常用方法
    3.1 File类的获取功能

public String getAbsolutePath():获取绝对路径

public String getPath():获取路径

public String getName() :获取名称

public String getParent():获取上层文件目录路径。若无,返回 null

public long length() :获取文件长度(即:字节数)。不能获取目录的长度。

public long lastModified() :获取最后一次的修改时间,毫秒值

如下的两个方法适用于文件目录:

public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组

public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组

代码示例:
@Test
public void test2(){
File file1 = new File(“hello.txt”);
File file2 = new File(“d:\io\hi.txt”);

System.out.println(file1.getAbsolutePath());
System.out.println(file1.getPath());
System.out.println(file1.getName());
System.out.println(file1.getParent());
System.out.println(file1.length());
System.out.println(new Date(file1.lastModified()));

System.out.println();

System.out.println(file2.getAbsolutePath());
System.out.println(file2.getPath());
System.out.println(file2.getName());
System.out.println(file2.getParent());
System.out.println(file2.length());
System.out.println(file2.lastModified());

}
@Test
public void test3(){
File file = new File(“D:\workspace_idea1\JavaSenior”);

String[] list = file.list();
for(String s : list){
    System.out.println(s);
}

System.out.println();

File[] files = file.listFiles();
for(File f : files){
    System.out.println(f);
}

}

复制代码
3.2 File类的重命名功能

public boolean renameTo(File dest):把文件重命名为指定的文件路径
注意:file1.renameTo(file2)为例:要想保证返回 true ,需要file1在硬盘中是存在的,且file2不能在硬盘中存在。

代码示例:
@Test
public void test4(){
File file1 = new File(“hello.txt”);
File file2 = new File(“D:\io\hi.txt”);

boolean renameTo = file2.renameTo(file1);
System.out.println(renameTo);

}

复制代码
3.3 File类的判断功能

public boolean isDirectory():判断是否是文件目录
public boolean isFile() :判断是否是文件
public boolean exists() :判断是否存在
public boolean canRead() :判断是否可读
public boolean canWrite() :判断是否可写
public boolean isHidden() :判断是否隐藏

代码示例:
@Test
public void test5(){
File file1 = new File(“hello.txt”);
file1 = new File(“hello1.txt”);

System.out.println(file1.isDirectory());
System.out.println(file1.isFile());
System.out.println(file1.exists());
System.out.println(file1.canRead());
System.out.println(file1.canWrite());
System.out.println(file1.isHidden());

System.out.println();

File file2 = new File("d:\\io");
file2 = new File("d:\\io1");
System.out.println(file2.isDirectory());
System.out.println(file2.isFile());
System.out.println(file2.exists());
System.out.println(file2.canRead());
System.out.println(file2.canWrite());
System.out.println(file2.isHidden());

}

复制代码
3.4 Flie类的创建功能

创建硬盘中对应的文件或文件目录
public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs() :创建文件目录。如果此文件目录存在,就不创建了。如果上层文件目录不存在,一并创建

代码示例:
@Test
public void test6() throws IOException {
File file1 = new File(“hi.txt”);
if(!file1.exists()){
//文件的创建
file1.createNewFile();
System.out.println(“创建成功”);
}else{//文件存在
file1.delete();
System.out.println(“删除成功”);
}

}
@Test
public void test7(){
//文件目录的创建
File file1 = new File(“d:\io\io1\io3”);

boolean mkdir = file1.mkdir();
if(mkdir){
    System.out.println("创建成功1");
}

File file2 = new File("d:\\io\\io1\\io4");

boolean mkdir1 = file2.mkdirs();
if(mkdir1){
    System.out.println("创建成功2");
}
//要想删除成功,io4文件目录下不能有子目录或文件
File file3 = new File("D:\\io\\io1\\io4");
file3 = new File("D:\\io\\io1");
System.out.println(file3.delete());

}

复制代码
3.5 File类的删除功能

删除磁盘中的文件或文件目录
public boolean delete():删除文件或者文件夹
删除注意事项:Java中的删除不走回收站。

  1. 内存解析

  2. 小练习
    利用 File 构造器,new 一个文件目录file
    1)在其中创建多个文件和目录
    2)编写方法,实现删除fle中指定文件的操作
    @Test
    public void test1() throws IOException {
    File file = new File(“E:\io\io1\hello.txt”);
    //创建一个与file同目录下的另外一个文件,文件名为:haha.txt
    File destFile = new File(file.getParent(),“haha.txt”);
    boolean newFile = destFile.createNewFile();
    if(newFile){
    System.out.println(“创建成功!”);
    }
    }

复制代码
判断指定目录下是否有后缀名为 .jpg 的文件,如果有,就输出该文件名称
public class FindJPGFileTest {

@Test
public void test1(){
    File srcFile = new File("d:\\code");

    String[] fileNames = srcFile.list();
    for(String fileName : fileNames){
        if(fileName.endsWith(".jpg")){
            System.out.println(fileName);
        }
    }
}
@Test
public void test2(){
    File srcFile = new File("d:\\code");

    File[] listFiles = srcFile.listFiles();
    for(File file : listFiles){
        if(file.getName().endsWith(".jpg")){
            System.out.println(file.getAbsolutePath());
        }
    }
}
/*
 * File类提供了两个文件过滤器方法
 * public String[] list(FilenameFilter filter)
 * public File[] listFiles(FileFilter filter)

 */
@Test
public void test3(){
    File srcFile = new File("d:\\code");

    File[] subFiles = srcFile.listFiles(new FilenameFilter() {

        @Override
        public boolean accept(File dir, String name) {
            return name.endsWith(".jpg");
        }
    });

    for(File file : subFiles){
        System.out.println(file.getAbsolutePath());
    }
}

}

复制代码
遍历指定目录所有文件名称,包括子文件目录中的文件。
拓展1:并计算指定目录占用空间的大小
拓展2:删除指定文件目录及其下的所有文件
public class ListFileTest {

public static void main(String[] args) {
    // 递归:文件目录
    /** 打印出指定目录所有文件名称,包括子文件目录中的文件 */

    //1.创建目录对象
    File file = new File("E:\\test");

    //2.打印子目录
    printSubFile(file);

}

/**
 * 递归方法遍历所有目录下的文件
 *
 * @param dir
 */
public static void printSubFile(File dir) {
    //打印子目录
    File[] files = dir.listFiles();
    for (File f : files) {
        if (f.isDirectory()) {//如果为文件目录,则递归调用自身
            printSubFile(f);
        } else {
            System.out.println(f.getAbsolutePath());//输出绝对路径
        }
    }
}

// 拓展1:求指定目录所在空间的大小
// 求任意一个目录的总大小
public long getDirectorySize(File file) {
    // file是文件,那么直接返回file.length()
    // file是目录,把它的下一级的所有大小加起来就是它的总大小
    long size = 0;
    if (file.isFile()) {
        size += file.length();
    } else {
        File[] allFiles = file.listFiles();// 获取file的下一级
        // 累加all[i]的大小
        for (File f : allFiles) {
            size += getDirectorySize(f);//f的大小
        }
    }
    return size;
}

/**
 * 拓展2:删除指定的目录
 */
public void deleteDirectory(File file) {
    // 如果file是文件,直接delete
    // 如果file是目录,先把它的下一级干掉,然后删除自己
    if (file.isDirectory()) {
        File[] allFiles = file.listFiles();
        //递归调用删除file下一级
        for (File f : allFiles) {
            deleteDirectory(f);
        }
    } else {
        //删除文件
        file.delete();
    }
}

}

复制代码
二、IO流概述

  1. 简述

IO 是 Input/Output 的缩写,I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

Java程序中,对于数据的输入输出操作以 “流(stream)” 的方式进行。

Java.IO 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

  1. 流的分类
    操作数据单位:字节流、字符流

对于文本文件(.txt,.java,.c,.cpp),使用字符流处理
对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,…),使用字节流处理

数据的流向:输入流、输出流

输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

流的角色:节点流、处理流
节点流:直接从数据源或目的地读写数据。

处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

图示:

  1. IO流的体系分类
    3.1 总体分类

红框为抽象基类,蓝框为常用IO流

3.2 常用的几个IO流结构

抽象基类节点流(或文件流)缓冲流(处理流的一种)InputStreamFileInputStream (read(byte[] buffer))BufferedInputStream (read(byte[] buffer))OutputSteamFileOutputStream (write(byte[] buffer,0,len)BufferedOutputStream (write(byte[] buffer,0,len) / flush()ReaderFileReader (read(char[] cbuf))BufferedReader (read(char[] cbuf) / readLine()WriterFileWriter (write(char[] cbuf,0,len)BufferedWriter (write(char[] cbuf,0,len) / flush()
3.3 对抽象基类的说明:

抽象基类字节流字符流输入流InputSteamReader输出流OutputSteamWriter

说明:Java的lO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

3.3.1InputSteam & Reader

InputStream和Reader是所有输入流的基类。

InputStream(典型实现:FileInputStream)

int read()
int read(byte[] b)
int read(byte[] b,int off,int len)

Reader(典型实现:FileReader)

int read()
int read(char[] c)
int read(char[] c,int off,int len)

程序中打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。

FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader。

InputSteam:

int read()
从输入流中读取数据的下一个字节。返回0到255范围内的int字节值。如果因为已经到达流末尾而没有可用的字节,则返回值-1。

int read(byte[] b)
从此输入流中将最多b.length个字节的数据读入一个byte数组中。如果因为已经到达流末尾而没有可用的字节,则返回值-1.否则以整数形式返回实际读取的字节数。

int read(byte[] b,int off,int len)
将输入流中最多len个数据字节读入byte数组。尝试读取len个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值-1。

public void close throws IOException
关闭此输入流并释放与该流关联的所有系统资源。

Reader:

int read()
读取单个字符。作为整数读取的字符,范围在0到65535之间(0x00-0xffff)(2个字节的 Unicode码),如果已到达流的末尾,则返回-1。

int read(char[] cbuf)
将字符读入数组。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。

int read(char[] cbuf,int off,int len)
将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。

public void close throws IOException
关闭此输入流并释放与该流关联的所有系统资源

3.3.2 OutputSteam & Writer

OutputStream 和 Writer也非常相似:

void write(int b/int c);
void write(byte[] b/char[] cbuf);
void write(byte[] b/char[] buff,int off,int len);
void flush();
void close();//需要先刷新,再关闭此流

因为字符流直接以字符作为操作单位,所以 Writer可以用字符串来替换字符数组,即以 String对象作为参数

void write(String str);
void write(String str,int off,int len);

FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputstream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter

OutputStream:

void write(int b)
将指定的字节写入此输出流。 write的常规协定是:向输出流写入一个字节。要写入的字节是参数b的八个低位。b的24个高位将被忽略。即写入0~255范围的

void write(byte[] b)
将 b.length 个字节从指定的byte数组写入此输出流。write(b)的常规协定是:应该与调用wite(b,0,b.length)的效果完全相同。

void write(byte[] b,int off,int len)
将指定byte数组中从偏移量off开始的len个字节写入此输出流。

public void flush()throws IOException
刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。

public void close throws IOException
关闭此输岀流并释放与该流关联的所有系统资源。

Writer:

void write(int c)
写入单个字符。要写入的字符包含在给定整数值的16个低位中,16高位被忽略。即写入0到65535之间的 Unicode码。

void write(char[] cbuf)
写入字符数组

void write(char[] cbuf,int off,int len)
写入字符数组的某一部分。从off开始,写入len个字符

void write(String str)
写入字符串。

void write(String str,int off,int len)
写入字符串的某一部分。

void flush()
刷新该流的缓冲,则立即将它们写入预期目标。

public void close throws IOException
关闭此输出流并释放与该流关联的所有系统资源

  1. 输入、输出标准化过程
    4.1 输入过程:
    ① 创建 File 类的对象,指明读取的数据的来源。(要求此文件一定要存在)
    ② 创建相应的输入流,将 File 类的对象作为参数,传入流的构造器中
    ③ 具体的读入过程:创建相应的 byte[] 或 char[]。
    ④ 关闭流资源
    说明:程序中出现的异常需要使用 try-catch-finally 处理。
    4.2 输出过程:
    ① 创建 File 类的对象,指明写出的数据的位置。(不要求此文件一定要存在)
    ② 创建相应的输出流,将 File 类的对象作为参数,传入流的构造器中
    ③ 具体的写出过程:write(char[]/byte[] buffer,0,len)
    ④ 关闭流资源
    说明:程序中出现的异常需要使用 try-catch-finally 处理。
    三、节点流(文件流)
  2. 文件字符流 FileReader 和 FileWriter 的使用
    1.1文件的输入
    从文件中读取到内存(程序)中
    步骤:

建立一个流对象,将已存在的一个文件加载进流 FileReader fr = new FileReader(new File(“Test. txt”));
创建一个临时存放数据的数组 char[] ch = new char[1024];
调用流对象的读取方法将流中的数据读入到数组中。 fr.read(ch);
关闭资源。 fr.close();

代码示例:
@Test
public void testFileReader1() {
FileReader fr = null;
try {
//1.File类的实例化
File file = new File(“hello.txt”);

    //2.FileReader流的实例化
    fr = new FileReader(file);

    //3.读入的操作
    //read(char[] cbuf):返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
    char[] cbuf = new char[5];
    int len;
    while((len = fr.read(cbuf)) != -1){
        String str = new String(cbuf,0,len);
        System.out.print(str);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if(fr != null){
        //4.资源的关闭
        try {
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
注意点:

read() 的理解:返回读入的一个字符。如果达到文件末尾,返回-1

异常的处理:为了保证流资源一定可以执行关闭操作。需要使用 try-catch-finally 处理

读入的文件一定要存在,否则就会报 FileNotFoundException。

1.2 文件的输出
从内存(程序)到硬盘文件中
步骤:

创建流对象,建立数据存放文件 File Writer fw = new File Writer(new File(“Test.txt”))

调用流对象的写入方法,将数据写入流 fw.write(“HelloWord”)

关闭流资源,并将流中的数据清空到文件中。 fw.close();

代码示例:
@Test
public void testFileWriter() {
FileWriter fw = null;
try {
//1.提供File类的对象,指明写出到的文件
File file = new File(“hello1.txt”);

    //2.提供FileWriter的对象,用于数据的写出
    fw = new FileWriter(file,false);

    //3.写出的操作
    fw.write("I have a dream!\n");
    fw.write("you need to have a dream!");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.流资源的关闭
    if(fw != null){

        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

}

复制代码
1.3 小练习
实现文本文件的复制操作
@Test
public void testFileReaderFileWriter() {
FileReader fr = null;
FileWriter fw = null;
try {
//1.创建File类的对象,指明读入和写出的文件
File srcFile = new File(“hello.txt”);
File destFile = new File(“hello2.txt”);

    //不能使用字符流来处理图片等字节数据
    //            File srcFile = new File("test.jpg");
    //            File destFile = new File("test1.jpg");

    //2.创建输入流和输出流的对象
    fr = new FileReader(srcFile);
    fw = new FileWriter(destFile);

    //3.数据的读入和写出操作
    char[] cbuf = new char[5];
    int len;//记录每次读入到cbuf数组中的字符的个数
    while((len = fr.read(cbuf)) != -1){
        //每次写出len个字符
        fw.write(cbuf,0,len);

    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.关闭流资源
    try {
        if(fw != null)
            fw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        if(fr != null)
            fr.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

}

复制代码
2. 文件字节流 FileInputSteam 和 FileOutputSteam 的使用
文件字节流操作与字符流操作类似,只是实例化对象操作和数据类型不同。
代码示例:
//使用字节流FileInputStream处理文本文件,可能出现乱码。
@Test
public void testFileInputStream() {
FileInputStream fis = null;
try {
//1. 造文件
File file = new File(“hello.txt”);

    //2.造流
    fis = new FileInputStream(file);

    //3.读数据
    byte[] buffer = new byte[5];
    int len;//记录每次读取的字节的个数
    while((len = fis.read(buffer)) != -1){

        String str = new String(buffer,0,len);
        System.out.print(str);

    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if(fis != null){
        //4.关闭资源
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
小练习
实现图片文件复制操作
@Test
public void testFileInputOutputStream() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.创建File对象
File srcFile = new File(“test.jpg”);
File destFile = new File(“test2.jpg”);

    //2.创建操流
    fis = new FileInputStream(srcFile);
    fos = new FileOutputStream(destFile);

    //3.复制的过程
    byte[] buffer = new byte[5];
    int len;
    while((len = fis.read(buffer)) != -1){
        fos.write(buffer,0,len);
    }

} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.关闭流
    if(fos != null){
        //
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(fis != null){
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
3. 注意点

定义路径时,可以用 / 或 \。

输出操作,对应的 File 可以不存在的。并不会报异常。

File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。

File 对应的硬盘中的文件如果存在:

如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖。
如果流使用的构造器是:FileWriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容。

读取文件时,必须保证文件存在,否则会报异常。

对于文本文件(.txt,.java,.c,.cpp),使用字符流处理

对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,…),使用字节流处理

四、缓冲流

  1. 缓冲流涉及到的类:

BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter

  1. 引入目的:

作用:提供流的读取、写入的速度

提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb

处理流与节点流的对比图示

  1. 使用说明

当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。

当使用 BufferedInputStream 读取字节文件时,BufferedInputStream 会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。

向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里。使用方法 flush() 可以强制将缓冲区的内容全部写入输出流。

关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流。

flush() 方法的使用:手动将 buffer 中内容写入文件。

如果是带缓冲区的流对象的 close() 方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。

代码示例:
3.1使用 BufferInputStream 和 BufferOutputStream 实现非文本文件的复制
@Test
public void testBufferedStream(){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.造文件
File srcFile = new File(“test.jpg”);
File destFile = new File(“test4.jpg”);
//2.造流
//2.1造节点流
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
//2.2造缓冲流,可以合并书写
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);

    //3.文件读取、写出操作
    byte[] buffer = new byte[1024];
    int len;
    while ((len = bis.read(buffer)) != -1){
        bos.write(buffer,0,len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.关闭流
    if (bos != null){
        try {
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (bis != null){

        try {
            bis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

}

复制代码
3.2 使用 BufferedReader 和 BufferedWriter 实现文本文件的复制
@Test
public void testBufferedReaderBufferedWriter(){
BufferedReader br = null;
BufferedWriter bw = null;
try {
//创建文件和相应的流
br = new BufferedReader(new FileReader(new File(“dbcp.txt”)));
bw = new BufferedWriter(new FileWriter(new File(“dbcp1.txt”)));

    //读写操作
    //方式一:使用char[]数组
    //            char[] cbuf = new char[1024];
    //            int len;
    //            while((len = br.read(cbuf)) != -1){
    //                bw.write(cbuf,0,len);
    //    //            bw.flush();
    //            }

    //方式二:使用String
    String data;
    while((data = br.readLine()) != null){
        //方法一:
        //                bw.write(data + "\n");//data中不包含换行符
        //方法二:
        bw.write(data);//data中不包含换行符
        bw.newLine();//提供换行的操作

    }


} catch (IOException e) {
    e.printStackTrace();
} finally {
    //关闭资源
    if(bw != null){

        try {
            bw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(br != null){
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
4. 小练习
4.1测试缓冲流和节点流文件复制速度
节点流实现复制方法
//指定路径下文件的复制
public void copyFile(String srcPath,String destPath){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.造文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);

    //2.造流
    fis = new FileInputStream(srcFile);
    fos = new FileOutputStream(destFile);

    //3.复制的过程
    byte[] buffer = new byte[1024];
    int len;
    while((len = fis.read(buffer)) != -1){
        fos.write(buffer,0,len);
    }

} catch (IOException e) {
    e.printStackTrace();
} finally {
    if(fos != null){
        //4.关闭流
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(fis != null){
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
缓冲流实现复制操作
//实现文件复制的方法
public void copyFileWithBuffered(String srcPath,String destPath){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;

try {
    //1.造文件
    File srcFile = new File(srcPath);
    File destFile = new File(destPath);
    //2.造流
    //2.1 造节点流
    FileInputStream fis = new FileInputStream((srcFile));
    FileOutputStream fos = new FileOutputStream(destFile);
    //2.2 造缓冲流
    bis = new BufferedInputStream(fis);
    bos = new BufferedOutputStream(fos);

    //3.复制的细节:读取、写入
    byte[] buffer = new byte[1024];
    int len;
    while((len = bis.read(buffer)) != -1){
        bos.write(buffer,0,len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.资源关闭
    //要求:先关闭外层的流,再关闭内层的流
    if(bos != null){
        try {
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    if(bis != null){
        try {
            bis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
测试二者速度
@Test
public void testCopyFileWithBuffered(){
long start = System.currentTimeMillis();

String srcPath = "C:\\Users\\Administrator\\Desktop\\01-视频.avi";
String destPath = "C:\\Users\\Administrator\\Desktop\\03-视频.avi";

copyFileWithBuffered(srcPath,destPath);

long end = System.currentTimeMillis();

System.out.println("复制操作花费的时间为:" + (end - start));//618 - 176

}

复制代码
4.2实现图片加密操作
加密操作

将图片文件通过字节流读取到程序中
将图片的字节流逐一进行 ^ 操作
将处理后的图片字节流输出

//图片的加密
@Test
public void test1() {

FileInputStream fis = null;
FileOutputStream fos = null;
try {
    fis = new FileInputStream("test.jpg");
    fos = new FileOutputStream("testSecret.jpg");

    byte[] buffer = new byte[20];
    int len;
    while ((len = fis.read(buffer)) != -1) {

        for (int i = 0; i < len; i++) {
            buffer[i] = (byte) (buffer[i] ^ 5);
        }

        fos.write(buffer, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
解密操作

将加密后图片文件通过字节流读取到程序中
将图片的字节流逐一进行 ^ 操作(原理:ABB = A)
将处理后的图片字节流输出

//图片的解密
@Test
public void test2() {

FileInputStream fis = null;
FileOutputStream fos = null;
try {
    fis = new FileInputStream("testSecret.jpg");
    fos = new FileOutputStream("test4.jpg");

    byte[] buffer = new byte[20];
    int len;
    while ((len = fis.read(buffer)) != -1) {
      
        for (int i = 0; i < len; i++) {
            buffer[i] = (byte) (buffer[i] ^ 5);
        }

        fos.write(buffer, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
4.3 统计文本字符出现次数
实现思路:

遍历文本每一个字符

字符出现的次数存在Map中

把map中的数据写入文件

@Test
public void testWordCount() {
FileReader fr = null;
BufferedWriter bw = null;
try {
//1.创建Map集合
Map<Character, Integer> map = new HashMap<Character, Integer>();

    //2.遍历每一个字符,每一个字符出现的次数放到map中
    fr = new FileReader("dbcp.txt");
    int c = 0;
    while ((c = fr.read()) != -1) {
        //int 还原 char
        char ch = (char) c;
        // 判断char是否在map中第一次出现
        if (map.get(ch) == null) {
            map.put(ch, 1);
        } else {
            map.put(ch, map.get(ch) + 1);
        }
    }

    //3.把map中数据存在文件count.txt
    //3.1 创建Writer
    bw = new BufferedWriter(new FileWriter("wordcount.txt"));

    //3.2 遍历map,再写入数据
    Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
    for (Map.Entry<Character, Integer> entry : entrySet) {
        switch (entry.getKey()) {
            case ' ':
                bw.write("空格=" + entry.getValue());
                break;
            case '\t'://\t表示tab 键字符
                bw.write("tab键=" + entry.getValue());
                break;
            case '\r'://
                bw.write("回车=" + entry.getValue());
                break;
            case '\n'://
                bw.write("换行=" + entry.getValue());
                break;
            default:
                bw.write(entry.getKey() + "=" + entry.getValue());
                break;
        }
        bw.newLine();
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //4.关流
    if (fr != null) {
        try {
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    if (bw != null) {
        try {
            bw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
五、转换流

  1. 简介

转换流提供了在字节流和字符流之间的转换

Java API提供了两个转换流:

InputstreamReader:将 Inputstream 转换为 Reader
OutputStreamWriter:将 Writer 转换为 OutputStream

字节流中的数据都是字符时,转成字符流操作更高效。

很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。

1.1 InputStreamReader
InputStreamReader 将一个字节的输入流转换为字符的输入流
解码:字节、字节数组 —>字符数组、字符串
构造器:

public InputStreamReader(InputStream in)

public InputStreamReader(Inputstream in,String charsetName)//可以指定编码集

1.2 OutputStreamWriter
OutputStreamWriter 将一个字符的输出流转换为字节的输出流
编码:字符数组、字符串 —> 字节、字节数组
构造器:

public OutputStreamWriter(OutputStream out)
public OutputStreamWriter(Outputstream out,String charsetName)//可以指定编码集

图示:

2.代码示例:
/**
综合使用InputStreamReader和OutputStreamWriter
*/
@Test
public void test1() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
//1.造文件、造流
File file1 = new File(“dbcp.txt”);
File file2 = new File(“dbcp_gbk.txt”);

    FileInputStream fis = new FileInputStream(file1);
    FileOutputStream fos = new FileOutputStream(file2);

    isr = new InputStreamReader(fis, "utf-8");
    osw = new OutputStreamWriter(fos, "gbk");

    //2.读写过程
    char[] cbuf = new char[20];
    int len;
    while ((len = isr.read(cbuf)) != -1){
        osw.write(cbuf,0,len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //3.关流
    if (isr != null){

        try {
            isr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (osw != null){
        try {
            osw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码

说明:文件编码的方式(比如:GBK),决定了解析时使用的字符集(也只能是GBK)。

  1. 编码集
    3.1 常见的编码表

ASCII:美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1:拉丁码表。欧洲码表用一个字节的8位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符
GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
Unicode:国际标准码,融合了目前人类使用的所字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

说明:

面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。

UTF-8变长编码表示

3.2 编码应用

编码:字符串–>字节数组

解码:字节数组–>字符串

转换流的编码应用

可以将字符按指定编码格式存储
可以对文本数据按指定编码格式来解读
指定编码表的动作由构造器完成

使用要求:
客户端/浏览器端 <----> 后台(java,GO,Python,Node.js,php) <----> 数据库
要求前前后后使用的字符集都要统一:UTF-8.
六、标准输入、输出流
1.简介
System.in:标准的输入流,默认从键盘输入
System.out:标准的输出流,默认从控制台输出
2. 主要方法
System 类的 setIn(InputStream is) 方式重新指定输入的流
System 类的 setOut(PrintStream ps) 方式重新指定输出的流。
3. 使用示例
从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,
直至当输入 e 或者 exit 时,退出程序。
设计思路
方法一:使用 Scanner 实现,调用 next() 返回一个字符串
方法二:使用 System.in 实现。System.in —> 转换流 —> BufferedReader的readLine()
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);

    while (true) {
        System.out.println("请输入字符串:");
        String data = br.readLine();
        if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
            System.out.println("程序结束");
            break;
        }

        String upperCase = data.toUpperCase();
        System.out.println(upperCase);

    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
4. 小练习
设计实现 Scanner 类
public class MyInput {
// Read a string from the keyboard
public static String readString() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    // Declare and initialize the string
    String string = "";

    // Get the string from the keyboard
    try {
        string = br.readLine();

    } catch (IOException ex) {
        System.out.println(ex);
    }

    // Return the string obtained from the keyboard
    return string;
}

// Read an int value from the keyboard
public static int readInt() {
    return Integer.parseInt(readString());
}

// Read a double value from the keyboard
public static double readDouble() {
    return Double.parseDouble(readString());
}

// Read a byte value from the keyboard
public static double readByte() {
    return Byte.parseByte(readString());
}

// Read a short value from the keyboard
public static double readShort() {
    return Short.parseShort(readString());
}

// Read a long value from the keyboard
public static double readLong() {
    return Long.parseLong(readString());
}

// Read a float value from the keyboard
public static double readFloat() {
    return Float.parseFloat(readString());
}

}

复制代码
七、打印流
PrintStream 和 PrintWriter 说明:

提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出
System.out 返回的是 PrintStream 的实例

@Test
public void test2() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File(“D:\IO\text.txt”));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 ‘\n’ 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {// 把标准输出流(控制台输出)改成文件
System.setOut(ps);
}

    for (int i = 0; i <= 255; i++) { // 输出ASCII字符
        System.out.print((char) i);
        if (i % 50 == 0) { // 每50个数据一行
            System.out.println(); // 换行
        }
    }


} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (ps != null) {
        ps.close();
    }
}

}

复制代码
八、数据流
DataInputStream 和 DataOutputStream 作用: 用于读取或写出基本数据类型的变量或字符串
示例代码:
将内存中的字符串、基本数据类型的变量写出到文件中。
@Test
public void test3(){
//1.造对象、造流
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new FileOutputStream(“data.txt”));
//数据输出
dos.writeUTF(“Bruce”);
dos.flush();//刷新操作,将内存的数据写入到文件
dos.writeInt(23);
dos.flush();
dos.writeBoolean(true);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
//3.关闭流
if (dos != null){
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

复制代码
将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中。
/*
注意点:读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致!
*/
@Test
public void test4(){
DataInputStream dis = null;
try {
//1.造对象、造流
dis = new DataInputStream(new FileInputStream(“data.txt”));
//2.从文件读入数据
String name = dis.readUTF();
int age = dis.readInt();
boolean isMale = dis.readBoolean();

    System.out.println("name:"+name);
    System.out.println("age:"+age);
    System.out.println("isMale:"+isMale);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    //3.关闭流
    if (dis != null){

        try {
            dis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

}

复制代码
九、对象流
1.对象流:
ObjectInputStream 和 ObjectOutputStream
2.作用:

ObjectOutputStream:内存中的对象—>存储中的文件、通过网络传输出去:序列化过程
ObjectInputStream:存储中的文件、通过网络接收过来 —>内存中的对象:反序列化过程

  1. 对象的序列化

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。

序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。

序列化是RMI(Remote Method Invoke-远程方法调用)过程的参数和返回值都必须实现的机制,RMI是JavaEE的基础。因此序列化机制是JavaEE平台的基础。

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出 NotserializableEXception 异常

Serializable
Externalizable

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID;
serialVersionUID 用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容
如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化。故建议显式声明。

简单来说,Java 的序列化机制是通过在运行时判断类的 serialversionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialversionUID 与本地相应实体类的 serialversionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

4.实现序列化的对象所属的类需要满足:

需要实现接口:Serializable(标识接口)

当前类提供一个全局常量:serialVersionUID(序列版本号)

除了当前 Person 类需要实现 Serializable 接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)

补充:ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量
5. 对象流的使用
5.1序列化代码实现
序列化:将对象写入磁盘或进行网络传输
要求被序列化对象必须实现序列化
@Test
public void testObjectOutputStream(){
ObjectOutputStream oos = null;

try {
    //1.创建对象,创建流
    oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
    //2.操作流
    oos.writeObject(new String("我爱北京天安门"));
    oos.flush();//刷新操作

    oos.writeObject(new Person("王铭",23));
    oos.flush();

    oos.writeObject(new Person("张学良",23,1001,new Account(5000)));
    oos.flush();

} catch (IOException e) {
    e.printStackTrace();
} finally {
    if(oos != null){
        //3.关闭流
        try {
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
5.2 反序列化代码实现
反序列化:将磁盘的对象数据源读出
@Test
public void testObjectInputStream(){
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(“object.dat”));

    Object obj = ois.readObject();
    String str = (String) obj;

    Person p = (Person) ois.readObject();
    Person p1 = (Person) ois.readObject();

    System.out.println(str);
    System.out.println(p);
    System.out.println(p1);

} catch (IOException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} finally {
    if(ois != null){
        try {
            ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

}

复制代码
十、任意存取文件流
RandomAccessFile 的使用
1.简介

RandomAccessFile 直接继承于 java.lang.Object 类,实现了 DataInput 和DataOutput 接口

RandomAccessFile 既可以作为一个输入流,又可以作为一个输出流

RandomAccessFile 类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件

支持只访问文件的部分内容
可以向已存在的文件后追加内容

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置

RandomaccessFile 类对象可以自由移动记录指针:

long getFilePointer():获取文件记录指针的当前位置
void seek(long pos):将文件记录指针定位到 pos 位置

构造器
public RandomAccessFile(File file,String mode)
public RandomAccessFile(String name,String mode)
2.使用说明:

如果 RandomAccessFile 作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。
如果写出到的文件存在,则会对原文件内容进行覆盖。(默认情况下,从头覆盖)
可以通过相关的操作,实现 RandomAccessFile **“插入”**数据的效果。借助 seek(int pos) 方法
创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定RandomAccessFile 的访问模式:

r:以只读方式打开
rw:打开以便读取和写入
rwd:打开以便读取和写入;同步文件内容的更新
rws:打开以便读取和写入;同步文件内容和元数据的更新

如果模式为只读 r ,则不会创建文件,而是会去读取一个已经存在的文件,读取的文件不存在则会出现异常。如果模式为 rw 读写,文件不存在则会去创建文件,存在则不会创建。

  1. 使用示例
    文件的读取和写出操作
    @Test
    public void test1() {

    RandomAccessFile raf1 = null;
    RandomAccessFile raf2 = null;
    try {
    //1.创建对象,创建流
    raf1 = new RandomAccessFile(new File(“test.jpg”),“r”);
    raf2 = new RandomAccessFile(new File(“test1.jpg”),“rw”);
    //2.操作流
    byte[] buffer = new byte[1024];
    int len;
    while((len = raf1.read(buffer)) != -1){
    raf2.write(buffer,0,len);
    }
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    //3.关闭流
    if(raf1 != null){
    try {
    raf1.close();
    } catch (IOException e) {
    e.printStackTrace();
    }

     }
     if(raf2 != null){
         try {
             raf2.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
    
     }
    

    }
    }

复制代码
使用 RandomAccessFile 实现数据的插入效果
@Test
public void test2(){
RandomAccessFile raf1 = null;
try {
raf1 = new RandomAccessFile(new File(“hello.txt”), “rw”);

    raf1.seek(3);//将指针调到角标为3的位置
    //            //方式一
    //            //保存指针3后面的所有数据到StringBuilder中
    //            StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
    //            byte[] buffer = new byte[20];
    //            int len;
    //            while ((len = raf1.read(buffer)) != -1){
    //                builder.append(new String(buffer,0,len));
    //            }

    //方式二
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[20];
    int len;
    while ((len = raf1.read(buffer)) != -1){
        baos.write(buffer);
    }
    //调回指针,写入“xyz”
    raf1.seek(3);
    raf1.write("xyz".getBytes());
    //将StringBuilder中的数据写入到文件中
    raf1.write(baos.toString().getBytes());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (raf1 != null){
        try {
            raf1.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

}

复制代码
十一、流的基本应用总结

流是用来处理数据的。

处理数据时,一定要先明确数据源,与数据目的地数据源可以是文件,可以是键盘数据目的地可以是文件、显示器或者其他设备

而流只是在帮助数据进行传输,并对传输的数据进行处理,比如过滤处理、转换处理等

除去RandomAccessFile类外所有的流都继承于四个基本数据流抽象类InputSteam、OutputSteam、Reader、Writer

不同的操作流对应的后缀均为四个抽象基类中的某一个

不同处理流的使用方式都是标准操作:

创建文件对象,创建相应的流
处理流数据
关闭流
用try-catch-finally处理异常

十二、NIO
Path、Paths、Files的使用,介绍比较简单,后期会再抽时间详细写有关 NIO 的博客。
1.NIO的使用说明:

Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的Java IO AP。
NIO 与原来的 IO 同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO是面向流的)、基于通道的IO操作。
NIO 将以更加高效的方式进行文件的读写操作。
JDK 7.0对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,称他为 NIO.2。

Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO
|-----java.nio.channels.Channel
|---- FileChannel:处理本地文件
|---- SocketChannel:TCP网络编程的客户端的Channel
|---- ServerSocketChannel:TCP网络编程的服务器端的Channel
|---- DatagramChannel:UDP网络编程中发送端和接收端的Channel

复制代码
2.Path接口 —JDK 7.0提供

早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。

NIO.2为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。

2.1Path的说明:
Path替换原有的File类。

在以前IO操作都是这样写的:

import java.io.File
File file = new File(“index.html”);

但在Java7中,我们可以这样写:

import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get(“index. html”);

2.2 Paths的使用

Paths 类提供的静态 get() 方法用来获取 Path 对象:
static Path get(String first, String….more):用于将多个字符串串连成路径
static Path get(URI uri):返回指定 uri 对应的 Path 路径

代码示例
@Test
public void test1(){
Path path1 = Paths.get(“hello.txt”);//new File(String filepath)

Path path2 = Paths.get("E:\\", "test\\test1\\haha.txt");//new File(String parent,String filename);

Path path3 = Paths.get("E:\\", "test");

System.out.println(path1);
System.out.println(path2);
System.out.println(path3);

}

复制代码
2.3 常用方法

String toString() : 返回调用 Path 对象的字符串表示形式
boolean startsWith(String path) : 判断是否以 path 路径开始
boolean endsWith(String path) : 判断是否以 path 路径结束
boolean isAbsolute() : 判断是否是绝对路径
Path getParent() :返回 Path 对象包含整个路径,不包含 Path 对象指定的文件路径
Path getRoot() :返回调用 Path 对象的根路径
Path getFileName() : 返回与调用 Path 对象关联的文件名
int getNameCount() : 返回 Path 根目录后面元素的数量
Path getName(int idx) : 返回指定索引位置 idx 的路径名称
Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
Path resolve(Path p) :合并两个路径,返回合并后的路径对应的 Path 对象
File toFile(): 将 Path 转化为 File 类的对象

代码示例
@Test
public void test2() {
Path path1 = Paths.get(“d:\”, “nio\nio1\nio2\hello.txt”);
Path path2 = Paths.get(“hello.txt”);

//		String toString() : 返回调用 Path 对象的字符串表示形式
System.out.println(path1);

//		boolean startsWith(String path) : 判断是否以 path 路径开始
System.out.println(path1.startsWith("d:\\nio"));
//		boolean endsWith(String path) : 判断是否以 path 路径结束
System.out.println(path1.endsWith("hello.txt"));
//		boolean isAbsolute() : 判断是否是绝对路径
System.out.println(path1.isAbsolute() + "~");
System.out.println(path2.isAbsolute() + "~");
//		Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
System.out.println(path1.getParent());
System.out.println(path2.getParent());
//		Path getRoot() :返回调用 Path 对象的根路径
System.out.println(path1.getRoot());
System.out.println(path2.getRoot());
//		Path getFileName() : 返回与调用 Path 对象关联的文件名
System.out.println(path1.getFileName() + "~");
System.out.println(path2.getFileName() + "~");
//		int getNameCount() : 返回Path 根目录后面元素的数量
//		Path getName(int idx) : 返回指定索引位置 idx 的路径名称
for (int i = 0; i < path1.getNameCount(); i++) {
    System.out.println(path1.getName(i) + "*****");
}

//		Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
System.out.println(path1.toAbsolutePath());
System.out.println(path2.toAbsolutePath());
//		Path resolve(Path p) :合并两个路径,返回合并后的路径对应的Path对象
Path path3 = Paths.get("d:\\", "nio");
Path path4 = Paths.get("nioo\\hi.txt");
path3 = path3.resolve(path4);
System.out.println(path3);

//		File toFile(): 将Path转化为File类的对象
File file = path1.toFile();//Path--->File的转换

Path newPath = file.toPath();//File--->Path的转换

}

复制代码
3.Files类
java.nio.file.Files 用于操作文件或目录的工具类
3.1 Files类常用方法

Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
要想复制成功,要求 path1 对应的物理上的文件存在。path1 对应的文件没有要求。

Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);

Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
要想执行成功,要求 path 对应的物理上的文件目录不存在。一旦存在,抛出异常。

Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件

要想执行成功,要求 path 对应的物理上的文件不存在。一旦存在,抛出异常。

void delete(Path path) : 删除一个文件/目录,如果不存在,执行报错

void deleteIfExists(Path path) : Path 对应的文件/目录如果存在,执行删除.如果不存在,正常执行结束

Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
要想执行成功,src 对应的物理上的文件需要存在,dest 对应的文件没有要求。

long size(Path path) : 返回 path 指定文件的大小

代码示例
@Test
public void test1() throws IOException{
Path path1 = Paths.get(“d:\nio”, “hello.txt”);
Path path2 = Paths.get(“atguigu.txt”);

//		Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
//要想复制成功,要求path1对应的物理上的文件存在。path1对应的文件没有要求。
//		Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);

//		Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
//要想执行成功,要求path对应的物理上的文件目录不存在。一旦存在,抛出异常。
Path path3 = Paths.get("d:\\nio\\nio1");
//		Files.createDirectory(path3);

//		Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
//要想执行成功,要求path对应的物理上的文件不存在。一旦存在,抛出异常。
Path path4 = Paths.get("d:\\nio\\hi.txt");
//		Files.createFile(path4);

//		void delete(Path path) : 删除一个文件/目录,如果不存在,执行报错
//		Files.delete(path4);

//		void deleteIfExists(Path path) : Path对应的文件/目录如果存在,执行删除.如果不存在,正常执行结束
Files.deleteIfExists(path3);

//		Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
//要想执行成功,src对应的物理上的文件需要存在,dest对应的文件没有要求。
//		Files.move(path1, path2, StandardCopyOption.ATOMIC_MOVE);

//		long size(Path path) : 返回 path 指定文件的大小
long size = Files.size(path2);
System.out.println(size);

}

复制代码

3.2 Files类常用方法:用于判断

boolean exists(Path path, LinkOption … opts) : 判断文件是否存在

boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
不要求此 path 对应的物理文件存在。

boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件

boolean isHidden(Path path) : 判断是否是隐藏文件
要求此 path 对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。

boolean isReadable(Path path) : 判断文件是否可读

boolean isWritable(Path path) : 判断文件是否可写

boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在

代码示例
@Test
public void test2() throws IOException{
Path path1 = Paths.get(“d:\nio”, “hello.txt”);
Path path2 = Paths.get(“atguigu.txt”);
// boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
System.out.println(Files.exists(path2, LinkOption.NOFOLLOW_LINKS));

//		boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
//不要求此path对应的物理文件存在。
System.out.println(Files.isDirectory(path1, LinkOption.NOFOLLOW_LINKS));

//		boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件

//		boolean isHidden(Path path) : 判断是否是隐藏文件
//要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。
//		System.out.println(Files.isHidden(path1));

//		boolean isReadable(Path path) : 判断文件是否可读
System.out.println(Files.isReadable(path1));
//		boolean isWritable(Path path) : 判断文件是否可写
System.out.println(Files.isWritable(path1));
//		boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
System.out.println(Files.notExists(path1, LinkOption.NOFOLLOW_LINKS));

}

复制代码
补充:

StandardOpenOption.READ:表示对应的 Channel 是可读的。
StandardOpenOption.WRITE:表示对应的 Channel 是可写的。
StandardOpenOption.CREATE:如果要写出的文件不存在,则创建。如果存在,忽略
StandardOpenOption.CREATE_NEW:如果要写出的文件不存在,则创建。如果存在,抛异常

3.3 Files类常用方法:用于操作内容

InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
DirectoryStream newDirectoryStream(Path path) : 打开 path 指定的目录

代码示例
@Test
public void test3() throws IOException{
Path path1 = Paths.get(“d:\nio”, “hello.txt”);

//		InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
InputStream inputStream = Files.newInputStream(path1, StandardOpenOption.READ);

//		OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
OutputStream outputStream = Files.newOutputStream(path1, StandardOpenOption.WRITE,StandardOpenOption.CREATE);


//		SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
SeekableByteChannel channel = Files.newByteChannel(path1, StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);

//		DirectoryStream<Path>  newDirectoryStream(Path path) : 打开 path 指定的目录
Path path2 = Paths.get("e:\\teach");
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path2);
Iterator<Path> iterator = directoryStream.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next());
}

}

File 类的使用

java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关
File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。
如果需要访问文件内容本身,则需要使用输入/输出流。
想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
File对象可以作为参数传递给流的构造器

常用构造器

public File(String pathname)
	以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
	绝对路径:是一个固定的路径,从盘符开始
	相对路径:是相对于某个位置开始
public File(String parent,String child)
	以parent为父路径,child为子路径创建File对象。
public File(File parent,String child)
	根据一个父File对象和子文件路径创建File对象

路径分隔符

路径中的每级目录之间用一个路径分隔符隔开。
路径分隔符和系统有关:
windows和DOS系统默认使用“\”来表示
UNIX和URL使用“/”来表示
Java程序支持跨平台运行,因此路径分隔符要慎用。
为了解决这个隐患,File类提供了一个常量:
public static final String separator。根据操作系统,动态的提供分隔符。
举例:
File file1 = new File("d:\\atguigu\\info.txt");
File file2 = new File("d:" + File.separator + "atguigu" + File.separator + "info.txt");
File file3 = new File("d:/atguigu");

常用方法

File类的获取功能

public String getAbsolutePath():获取绝对路径
public String getPath() :获取路径
public String getName() :获取名称
public String getParent():获取上层文件目录路径。若无,返回null
public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified() :获取最后一次的修改时间,毫秒值
public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组

File类的重命名功能

public boolean renameTo(File dest):把文件重命名为指定的文件路径

File类的判断功能

public boolean isDirectory():判断是否是文件目录
public boolean isFile() :判断是否是文件
public boolean exists() :判断是否存在
public boolean canRead() :判断是否可读
public boolean canWrite() :判断是否可写
public boolean isHidden() :判断是否隐藏

File类的创建功能

public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建

注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下。

File类的删除功能

public boolean delete():删除文件或者文件夹
删除注意事项:
Java中的删除不走回收站。
要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

File dir1 = new File("D:/IOTest/dir1");
if (!dir1.exists()) { // 如果D:/IOTest/dir1不存在,就创建为目录
	dir1.mkdir();
}
// 创建以dir1为父目录,名为"dir2"的File对象
File dir2 = new File(dir1, "dir2");
if (!dir2.exists()) { // 如果还不存在,就创建为目录
	dir2.mkdirs();
}
File dir4 = new File(dir1, "dir3/dir4");
if (!dir4.exists()) {
	dir4.mkdirs();
}
// 创建以dir2为父目录,名为"test.txt"的File对象
File file = new File(dir2, "test.txt");
if (!file.exists()) { // 如果还不存在,就创建为文件
	file.createNewFile();
}

IO流原理及流的分类

Google I/O 寓为“开放中创新”
(Innovation in the Open)
Input/Output
二进制1,0
13.2 IO流原理及流的分类
I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于
处理设备之间的数据传输。如读/写文件,网络通讯等。
Java程序中,对于数据的输入/输出操作以“流(stream)” 的
方式进行。
java.io包下提供了各种“流”类和接口,用以获取不同种类的
数据,并通过标准的方法输入或输出数据。
Java IO原理

输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
13.2 IO流原理及流的分类
流的分类
按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流
(抽象基类) 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

  1. Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个
    抽象基类派生的。
  2. 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

节点流

FileInputStream和FileOutputStream

FileReader和FileWriter

处理流

BufferedInputStream和BufferedOutputSteam

BufferedReader和BufferedWriter

转换流

InputStreamReader和OutputStreamWriter

节点流和处理流
 节点流:直接从数据源或目的地读写数据
 处理流:不直接连接到数据源或目的地,而是“连接”在已存
在的流(节点流或处理流)之上,通过对数据的处理为程序提
供更为强大的读写功能。
13.2 IO流原理及流的分类
InputStream & Reader
InputStream 和 Reader 是所有输入流的基类。
InputStream(典型实现:FileInputStream)
int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
Reader(典型实现:FileReader)
int read()
int read(char [] c)
int read(char [] c, int off, int len)
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资
源,所以应该显式关闭文件 IO 资源。
FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream
用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
13.2 IO流原理及流的分类
InputStream
 int read()
从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因
为已经到达流末尾而没有可用的字节,则返回值 -1。
 int read(byte[] b)
从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已
经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取
的字节数。
 int read(byte[] b, int off,int len)
将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取
的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于
文件末尾而没有可用的字节,则返回值 -1。
 public void close() throws IOException
关闭此输入流并释放与该流关联的所有系统资源。
13.2 IO流原理及流的分类
Reader
 int read()
读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个
字节的Unicode码),如果已到达流的末尾,则返回 -1
 int read(char[] cbuf)
将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
 int read(char[] cbuf,int off,int len)
将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字
符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
 public void close() throws IOException
关闭此输入流并释放与该流关联的所有系统资源。
13.2 IO流原理及流的分类
OutputStream & Writer
 OutputStream 和 Writer 也非常相似:
 void write(int b/int c);
 void write(byte[] b/char[] cbuf);
 void write(byte[] b/char[] buff, int off, int len);
 void flush();
 void close(); 需要先刷新,再关闭此流
 因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,
即以 String 对象作为参数
 void write(String str);
 void write(String str, int off, int len);
 FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream
用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter
13.2 IO流原理及流的分类
 void write(int b)
OutputStream
将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写
入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
 void write(byte[] b)
将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该
与调用 write(b, 0, b.length) 的效果完全相同。
 void write(byte[] b,int off,int len)
将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
 public void flush()throws IOException
刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立
即写入它们预期的目标。
 public void close() throws IOException
关闭此输出流并释放与该流关联的所有系统资源。
13.2 IO流原理及流的分类
Writer
 void write(int c)
写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即
写入0 到 65535 之间的Unicode码。
 void write(char[] cbuf)
写入字符数组。
 void write(char[] cbuf,int off,int len)
写入字符数组的某一部分。从off开始,写入len个字符
 void write(String str)
写入字符串。
 void write(String str,int off,int len)
写入字符串的某一部分。
 void flush()
刷新该流的缓冲,则立即将它们写入预期目标。
 public void close() throws IOException
关闭此输出流并释放与该流关联的所有系统资源。
13.2 IO流原理及流的分类
13-3 节点流(或文件流)
读取文件
1.建立一个流对象,将已存在的一个文件加载进流。
 FileReader fr = new FileReader(new File(“Test.txt”));
2.创建一个临时存放数据的数组。
 char[] ch = new char[1024];
3.调用流对象的读取方法将流中的数据读入到数组中。
 fr.read(ch);
4. 关闭资源。
 fr.close();
13.3 节点流(或文件流)
FileReader fr = null;
try {
fr = new FileReader(new File(“c:\test.txt”));
char[] buf = new char[1024];
int len;
while ((len = fr.read(buf)) != -1) {
System.out.print(new String(buf, 0, len));
}
} catch (IOException e) {
System.out.println(“read-Exception :” + e.getMessage());
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
System.out.println(“close-Exception :” + e.getMessage());
}
}
}
13.3 节点流(或文件流)
写入文件
1.创建流对象,建立数据存放文件
 FileWriter fw = new FileWriter(new File(“Test.txt”));
2.调用流对象的写入方法,将数据写入流
 fw.write(“atguigu-songhongkang”);
3.关闭流资源,并将流中的数据清空到文件中。
 fw.close();
13.3 节点流(或文件流)
FileWriter fw = null;
try {
fw = new FileWriter(new File(“Test.txt”));
fw.write(“atguigu-songhongkang”);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
13.3 节点流(或文件流)
 定义文件路径时,注意:可以用“/”或者“\”。
 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文
件将被覆盖。
 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖,
在文件内容末尾追加内容。
 在读取文件时,必须保证该文件已存在,否则报异常。
 字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
 字符流操作字符,只能操作普通文本文件。最常见的文本文
件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文
本文件。
13.3 节点流(或文件流):注意点
13-4 缓冲流
13.4 处理流之一:缓冲流
 为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类
时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)的缓冲区。
 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
BufferedInputStream 和 BufferedOutputStream
BufferedReader 和 BufferedWriter
 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从
文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中
读取下一个8192个字节数组。
 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,
BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法
flush()可以强制将缓冲区的内容全部写入输出流
 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也
会相应关闭内层节点流
 flush()方法的使用:手动将buffer中内容写入文件
 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷
新缓冲区,关闭后不能再写出
13.4 处理流之一:缓冲流
13.4 处理流之一:缓冲流
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 创建缓冲流对象:它是处理流,是对节点流的包装
br = new BufferedReader(new FileReader(“d:\IOTest\source.txt”));
bw = new BufferedWriter(new FileWriter(“d:\IOTest\dest.txt”));
String str;
while ((str = br.readLine()) != null) { // 一次读取字符文本文件的一行字符
bw.write(str); // 一次写入一行字符串
bw.newLine(); // 写入行分隔符
}
bw.flush(); // 刷新缓冲区
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭IO流对象
try {
if (bw != null) {
bw.close(); // 关闭过滤流时,会自动关闭它所包装的底层节点流
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
练 习

  1. 分别使用节点流:FileInputStream、FileOutputStream和缓冲流:
    BufferedInputStream、BufferedOutputStream实现文本文件/图片/视频文件的
    复制。并比较二者在数据复制方面的效率
  2. 实现图片加密操作。
    提示:
  3. 获取文本上每个字符出现的次数
    提示:遍历文本的每一个字符;字符及出现的次数保存在Map中;将Map中数据
    写入文件
    13.4 处理流之一:缓冲流
    13-5 转换流
    转换流提供了在字节流和字符流之间的转换
    Java API提供了两个转换流:
     InputStreamReader:将InputStream转换为Reader
     OutputStreamWriter:将Writer转换为OutputStream
     字节流中的数据都是字符时,转成字符流操作更高效。
     很多时候我们使用转换流来处理文件乱码问题。实现编码和
    解码的功能。
    13.5 处理流之二:转换流
    InputStreamReader
     实现将字节的输入流按指定字符集转换为字符的输入流。
     需要和InputStream“套接”。
     构造器
     public InputStreamReader(InputStream in)
     public InputSreamReader(InputStream in,String charsetName)
    如: Reader isr = new InputStreamReader(System.in,”gbk”);
    指定字符集
    13.5 处理流之二:转换流
    OutputStreamWriter
     实现将字符的输出流按指定字符集转换为字节的输出流。
     需要和OutputStream“套接”。
     构造器
     public OutputStreamWriter(OutputStream out)
     public OutputSreamWriter(OutputStream out,String charsetName)
    13.5 处理流之二:转换流
    utf8.txt
    InputStreamReader
    (utf-8)
    字节流
    程序
    字符流
    OutputStreamWriter
    (gbk)
    gbk.txt
    字符流
    字节流
    13.5 处理流之二:转换流
    13.5 处理流之二:转换流
    public void testMyInput() throws Exception {
    FileInputStream fis = new FileInputStream(“dbcp.txt”);
    FileOutputStream fos = new FileOutputStream(“dbcp5.txt”);
    InputStreamReader isr = new InputStreamReader(fis, “GBK”);
    OutputStreamWriter osw = new OutputStreamWriter(fos, “GBK”);
    BufferedReader br = new BufferedReader(isr);
    BufferedWriter bw = new BufferedWriter(osw);
    String str = null;
    while ((str = br.readLine()) != null) {
    bw.write(str);
    bw.newLine();
    bw.flush();
    }
    bw.close();
    br.close();
    }
     编码表的由来
    计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识
    别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。
    这就是编码表。
     常见的编码表
     ASCII:美国标准信息交换码。
     用一个字节的7位可以表示。
     ISO8859-1:拉丁码表。欧洲码表
     用一个字节的8位表示。
     GB2312:中国的中文编码表。最多两个字节编码所有字符
     GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
     Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的
    字符码。所有的文字都用两个字节来表示。
     UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
    补充:字符编码
    13.5 处理流之二:转换流
    13.5 处理流之二:转换流
     在Unicode出现之前,所有的字符集都是和具体编码方案绑定在一起的(即字
    符集≈编码方式),都是直接将字符和最终字节流绑定死了。
     GBK等双字节编码方式,用最高位是1或0表示两个字节和一个字节。
     Unicode不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用
    一个字节表示就够了,第二个问题是如何才能区别Unicode和ASCII?计算机
    怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果
    和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,
    就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时
    间内无法推广,直到互联网的出现。
     面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的
    编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
     Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯
    一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的
    Unicode编码是UTF-8和UTF-16。
    补充:字符编码
    13.5 处理流之二:转换流
    补充:字符编码
    13.5 处理流之二:转换流
    Unicode符号范围 | UTF-8编码方式
    (十六进制) | (二进制)
    —————————————————————–
    0000 0000-0000 007F | 0xxxxxxx(兼容原来的ASCII)
    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    补充:字符编码
    13.5 处理流之二:转换流
    ANSI编码,通常指的是平台的默认编码,
    例如英文操作系统中是ISO-8859-1,中文系
    统是GBK
    Unicode字符集只是定义了字符的集合和
    唯一编号,Unicode编码,则是对UTF-8、
    UCS-2/UTF-16等具体编码方案的统称而
    已,并不是具体的编码方案。
    编码:字符串字节数组
    解码:字节数组字符串
    转换流的编码应用
     可以将字符按指定编码格式存储
     可以对文本数据按指定编码格式来解读
     指定编码表的动作由构造器完成
    补充:字符编码
    13.5 处理流之二:转换流
    13-6 标准输入、输出流
     System.in和System.out分别代表了系统标准的输入和输出设备
     默认输入设备是:键盘,输出设备是:显示器
     System.in的类型是InputStream
     System.out的类型是PrintStream,其是OutputStream的子类
    FilterOutputStream 的子类
     重定向:通过System类的setIn,setOut方法对默认设备进行改变。
     public static void setIn(InputStream in)
     public static void setOut(PrintStream out)
    13.6 处理流之三:标准输入、输出流(了解)
    例 题
    从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续
    进行输入操作,直至当输入“e”或者“exit”时,退出程序。
    13.6 处理流之三:标准输入、输出流(了解)
    13.6 处理流之三:标准输入、输出流(了解)
    System.out.println(“请输入信息(退出输入e或exit):”);
    // 把"标准"输入流(键盘输入)这个字节流包装成字符流,再包装成缓冲流
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String s = null;
    try {
    while ((s = br.readLine()) != null) { // 读取用户输入的一行数据 --> 阻塞程序
    if (“e”.equalsIgnoreCase(s) || “exit”.equalsIgnoreCase(s)) {
    System.out.println(“安全退出!!”);
    break;
    }
    // 将读取到的整行字符串转成大写输出
    System.out.println(“–>:” + s.toUpperCase());
    System.out.println(“继续输入信息”);
    }
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    try {
    if (br != null) {
    br.close(); // 关闭过滤流时,会自动关闭它包装的底层节点流
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    练 习
    Create a program named MyInput.java: Contain the methods for reading int,
    double, float, boolean, short, byte and String values from the keyboard.
    Scanner s = new Scanner(System.in);
    s.nextInt();
    s.next();
    class MyInput{
    Scanner s = new Scanner(System.in);
    public String readString(){
    return s.next();
    }
    ….
    }
    13.6 处理流之三:标准输入、输出流(了解)
    13-7 打印流
    实现将基本数据类型的数据格式转化为字符串输出
    打印流:PrintStream和PrintWriter
     提供了一系列重载的print()和println()方法,用于多种数据类型的输出
     PrintStream和PrintWriter的输出不会抛出IOException异常
     PrintStream和PrintWriter有自动flush功能
     PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。
    在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
     System.out返回的是PrintStream的实例
    13.7 处理流之四:打印流(了解)
    PrintStream ps = null;
    try {
    FileOutputStream fos = new FileOutputStream(new File(“D:\IO\text.txt”));
    // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 ‘\n’ 时都会刷新输出缓冲区)
    ps = new PrintStream(fos, true);
    if (ps != null) {// 把标准输出流(控制台输出)改成文件
    System.setOut(ps);
    }
    for (int i = 0; i <= 255; i++) { // 输出ASCII字符
    System.out.print((char) i);
    if (i % 50 == 0) { // 每50个数据一行
    System.out.println(); // 换行
    }
    }
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } finally {
    if (ps != null) {
    ps.close();
    }
    }
    13.7 处理流之四:打印流(了解)
    13-8 数据流
     为了方便地操作Java语言的基本数据类型和String的数据,可以使用数据流。
     数据流有两个类:(用于读取和写出基本数据类型、String类的数据)
     DataInputStream 和 DataOutputStream
     分别“套接”在 InputStream 和 OutputStream 子类的流上
     DataInputStream中的方法
    boolean readBoolean() byte readByte()
    char readChar() float readFloat()
    double readDouble() short readShort()
    long readLong() int readInt()
    String readUTF() void readFully(byte[] b)
     DataOutputStream中的方法
     将上述的方法的read改为相应的write即可。
    13.8 处理流之五:数据流(了解)
    13.8 处理流之五:数据流(了解)
    DataOutputStream dos = null;
    try { // 创建连接到指定文件的数据输出流对象
    dos = new DataOutputStream(new FileOutputStream(“destData.dat”));
    dos.writeUTF(“我爱北京天安门”); // 写UTF字符串
    dos.writeBoolean(false); // 写入布尔值
    dos.writeLong(1234567890L); // 写入长整数
    System.out.println(“写文件成功!”);
    } catch (IOException e) {
    e.printStackTrace();
    } finally { // 关闭流对象
    try {
    if (dos != null) {
    // 关闭过滤流时,会自动关闭它包装的底层节点流
    dos.close();
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    13.8 处理流之五:数据流(了解)
    DataInputStream dis = null;
    try {
    dis = new DataInputStream(new FileInputStream(“destData.dat”));
    String info = dis.readUTF();
    boolean flag = dis.readBoolean();
    long time = dis.readLong();
    System.out.println(info);
    System.out.println(flag);
    System.out.println(time);
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (dis != null) {
    try {
    dis.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    13-9 对象流
    处理流之六:对象流
     ObjectInputStream和OjbectOutputSteam
     用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可
    以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
     序列化:用ObjectOutputStream类保存基本类型数据或对象的机制
     反序列化:用ObjectInputStream类读取基本类型数据或对象的机制
     ObjectOutputStream和ObjectInputStream不能序列化static和transient修
    饰的成员变量
    13.9 处理流之六:对象流
    对象的序列化
    对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从
    而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传
    输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原
    来的Java对象
    序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,
    使其在保存和传输时可被还原
    序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返
    回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是
    JavaEE 平台的基础
    如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可
    序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。
    否则,会抛出NotSerializableException异常
    Serializable
    Externalizable
    13.9 处理流之六:对象流
    凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
    private static final long serialVersionUID;
    serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象
    进行版本控制,有关各版本反序列化时是否兼容。
    如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自
    动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议,
    显式声明。
     简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验
    证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的
    serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同
    就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异
    常。(InvalidCastException)
    13.9 处理流之六:对象流
    对象的序列化
    使用对象流序列化对象
    若某个类实现了 Serializable 接口,该类的对象就是可序列化的:
    创建一个 ObjectOutputStream
    调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
    注意写出一次,操作flush()一次
    反序列化
    创建一个 ObjectInputStream
    调用 readObject() 方法读取流中的对象
    强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个
    引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的
    Field 的类也不能序列化
    13.9 处理流之六:对象流
    //序列化:将对象写入到磁盘或者进行网络传输。
    //要求对象必须实现序列化
    //反序列化:将磁盘中的对象数据源读出。
    13.9 处理流之六:对象流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“data.txt"));
    Person p = new Person(“韩梅梅”, 18, “中华大街”, new Pet());
    oos.writeObject§;
    oos.flush();
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“data.txt"));
    Person p1 = (Person)ois.readObject();
    System.out.println(p1.toString());
    ois.close();
    谈谈你对java.io.Serializable接口的理解,我们知道它用于序列化,
    是空方法接口,还有其它认识吗?
     实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后
    完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机
    制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创
    建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里
    准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必
    关心字节的顺序或者其他任何细节。
     由于大部分作为参数的类如String、Integer等都实现了
    java.io.Serializable的接口,也可以利用多态的性质,作为参数使接口更
    灵活。
    13.9 处理流之六:对象流
    13-10 随机存取文件流
    RandomAccessFile 类
    RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并
    且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也
    可以写。
    RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意
    地方来读、写文件
    支持只访问文件的部分内容
    可以向已存在的文件后追加内容
    RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。
    RandomAccessFile 类对象可以自由移动记录指针:
    long getFilePointer():获取文件记录指针的当前位置
    void seek(long pos):将文件记录指针定位到 pos 位置
    13.10 随机存取文件流
    RandomAccessFile 类
    构造器
    public RandomAccessFile(File file, String mode)
    public RandomAccessFile(String name, String mode)
    创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指
    定 RandomAccessFile 的访问模式:
    r: 以只读方式打开
    rw:打开以便读取和写入
    rwd:打开以便读取和写入;同步文件内容的更新
    rws:打开以便读取和写入;同步文件内容和元数据的更新
     如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件,
    如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不
    存在则会去创建文件,如果存在则不会创建。
    13.10 随机存取文件流
    RandomAccessFile 类
    13.10 随机存取文件流
    我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能,
    用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与
    被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次
    暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上
    一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以
    自己实现下。
    读取文件内容
    RandomAccessFile raf = new RandomAccessFile(“test.txt”, “rw”);
    raf.seek(5);
    byte [] b = new byte[1024];
    int off = 0;
    int len = 5;
    raf.read(b, off, len);
    String str = new String(b, 0, len);
    System.out.println(str);
    raf.close();
    13.10 随机存取文件流
    写入文件内容
    RandomAccessFile raf = new RandomAccessFile(“test.txt”, “rw”);
    raf.seek(5);
    //先读出来
    String temp = raf.readLine();
    raf.seek(5);
    raf.write(“xyz”.getBytes());
    raf.write(temp.getBytes());
    raf.close();
    13.10 随机存取文件流
    13.10 随机存取文件流 写入文件内容
    RandomAccessFile raf1 = new RandomAccessFile(“hello.txt”, “rw”);
    raf1.seek(5);
    //方式一:
    //StringBuilder info = new StringBuilder((int) file.length());
    //byte[] buffer = new byte[10];
    //int len;
    //while((len = raf1.read(buffer)) != -1){
    info += new String(buffer,0,len);
    //info.append(new String(buffer,0,len));
    //}
    //方式二:
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[10];
    int len;
    while((len = raf1.read(buffer)) != -1){
    baos.write(buffer, 0, len);
    }
    raf1.seek(5);
    raf1.write(“xyz”.getBytes());
    raf1.write(baos.toString().getBytes());
    baos.close();
    raf1.close();
    流的基本应用小节
     流是用来处理数据的。
     处理数据时,一定要先明确数据源,与数据目的地
     数据源可以是文件,可以是键盘。
     数据目的地可以是文件、显示器或者其他设备。
     而流只是在帮助数据进行传输,并对传输的数据进行处理,比如过滤处理、
    转换处理等。
    13-11 NIO.2中Path、
    Paths、Files类的使用
    Java NIO 概述
     Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新
    的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目
    的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于
    通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
     Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网
    络编程NIO。
    |-----java.nio.channels.Channel
    |-----FileChannel:处理本地文件
    |-----SocketChannel:TCP网络编程的客户端的Channel
    |-----ServerSocketChannel:TCP网络编程的服务器端的Channel
    |-----DatagramChannel:UDP网络编程中发送端和接收端的Channel
    13-11 NIO.2中Path、Paths、Files类的使用
    NIO. 2
     随着 JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对
    文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。
    因为 NIO 提供的一些功能,NIO已经成为文件处理中越来越重要
    的部分。
    13-11 NIO.2中Path、Paths、Files类的使用
    Path、Paths和Files核心API
     早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所
    提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异
    常信息。
     NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描
    述了目录结构中文件的位置。Path可以看成是File类的升级版本,实际引用的资
    源也可以不存在。
     在以前IO操作都是这样写的:
    import java.io.File;
    File file = new File(“index.html”);
     但在Java7 中,我们可以这样写:
    import java.nio.file.Path;
    import java.nio.file.Paths;
    Path path = Paths.get(“index.html”);
    13-11 NIO.2中Path、Paths、Files类的使用
    Path、Paths和Files核心API
     同时,NIO.2在java.nio.file包下还提供了Files、Paths工具类,Files包含
    了大量静态的工具方法来操作文件;Paths则包含了两个返回Path的静态
    工厂方法。
     Paths 类提供的静态 get() 方法用来获取 Path 对象:
    static Path get(String first, String … more) : 用于将多个字符串串连成路径
    static Path get(URI uri): 返回指定uri对应的Path路径
    13-11 NIO.2中Path、Paths、Files类的使用
    Path接口
     Path 常用方法:
     String toString() : 返回调用 Path 对象的字符串表示形式
     boolean startsWith(String path) : 判断是否以 path 路径开始
     boolean endsWith(String path) : 判断是否以 path 路径结束
     boolean isAbsolute() : 判断是否是绝对路径
     Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
     Path getRoot() :返回调用 Path 对象的根路径
     Path getFileName() : 返回与调用 Path 对象关联的文件名
     int getNameCount() : 返回Path 根目录后面元素的数量
     Path getName(int idx) : 返回指定索引位置 idx 的路径名称
     Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
     Path resolve(Path p) :合并两个路径,返回合并后的路径对应的Path对象
     File toFile(): 将Path转化为File类的对象
    13-11 NIO.2中Path、Paths、Files类的使用
    Files 类
     java.nio.file.Files 用于操作文件或目录的工具类。
     Files常用方法:
     Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
     Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录  Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
     void delete(Path path) : 删除一个文件/目录,如果不存在,执行报错
     void deleteIfExists(Path path) : Path对应的文件/目录如果存在,执行删除
     Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
     long size(Path path) : 返回 path 指定文件的大小
    13-11 NIO.2中Path、Paths、Files类的使用
    Files 类
     Files常用方法:用于判断
     boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
     boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
     boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件
     boolean isHidden(Path path) : 判断是否是隐藏文件
     boolean isReadable(Path path) : 判断文件是否可读
     boolean isWritable(Path path) : 判断文件是否可写
     boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
     Files常用方法:用于操作内容
     SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连
    接,how 指定打开方式。
     DirectoryStream newDirectoryStream(Path path) : 打开 path 指定的目录
     InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
     OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象

网络编程

一、概述

计算机网络:

把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息共享硬件、软件、数据信息等资源。

网络编程的目的: 直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯

实现网络通信需要解决的两个问题:

如何准确地定位网络上一台或多台主机;定位主机上的特定的应用
找到主机后如何可靠高效地进行数据传输

二、网络通讯要素

解决问题一:IP 和端口号
解决问题二:提供网络通信协议:TCP/IP 参考模型(应用层、传输层、网络层、物理+数据链路层)

网络通讯协议

通讯过程

  1. 通讯要素一:IP和端口号
    3.1 IP的理解

IP:唯一的标识 Internet 上的计算机(通信实体)

在Java中使用InetAddress类代表IP

IP分类:IPv4 和 IPv6 ; 万维网 和 局域网

域名: 通过域名解析服务器将域名解析为IP地址,如: www.baidu.com www.mi.com www.jd.com

域名解析:域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。

本地回路地址:127.0.0.1 对应着:localhost

3.2 端口号:
用于标识正在计算机上运行的进程。

要求:不同的进程不同的端口号
范围:被规定为一个 16 位的整数 0~65535。
分类:

公认端口:0~1023.被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,TeInet占用端口23)。
注册端口:1024~49151.分配给用户进程或应用程序。(如:Tomcat占用端口8080,MSQL占用端口3306,Oracle占用端口1521等)。
动态私有端口:49152~65535。

3.3 InetAddress类
此类的一个对象就代表着一个具体的IP地址
3.2.1实例化
getByName(String host) /getLocalHost()
3.2.2常用方法
getHostName() / getHostAddress()
public class InetAddressTest {
public static void main(String[] args) {
try {
//File file = new File(“hello.txt”);
InetAddress inet1 = InetAddress.getByName(“192.168.10.14”);

        System.out.println(inet1);

        InetAddress inet2 = InetAddress.getByName("www.baidu.com");
        System.out.println(inet2);

        InetAddress inet3 = InetAddress.getByName("127.0.0.1");
        System.out.println(inet3);

        //获取本地ip
        InetAddress inet4 = InetAddress.getLocalHost();
        System.out.println(inet4);

        //getHostName()
        System.out.println(inet2.getHostName());
        //getHostAddress()
        System.out.println(inet2.getHostAddress());

    } catch (UnknownHostException e) {
        e.printStackTrace();
    }
}

复制代码
4. 通信要素二:网络通信协议
4.1分层模型

4.2 TCP和UDP的区别
TCP协议:

使用TCP协议前,须先建立TCP连接,形成传输数据通道

传输前,采用“三次握手”方式,点对点通信,是可靠的

TCP协议进行通信的两个应用进程:客户端、服务端。

在连接中可进行大数据量的传输

传输完毕,需释放已建立的连接,效率低

UDP协议:

将数据、源、目的封装成数据包,不需要建立连接

每个数据报的大小限制在64K内

发送不管对方是否准备好,接收方收到也不确认,故是不可靠的

可以广播发送

发送数据结束时无需释放资源,开销小,速度快

4.3 TCP三次握手和四次挥手

  1. 套接字Socket
    端口号与 IP 地址的组合得出一个网络套接字:Socket

利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。

网络上具有唯一标识的 IP 地址和端口号组合在一起才能构成唯一能识别的标识符套接字。

通信的两端都要有 Socket,是两台机器间通信的端点。

网络通信其实就是 Socket 间的通信。

Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输。

一般主动发起通信的应用程序属客户端,等待通信请求的为服务端

Socket 分类

流套接字(stream socket):使用TCP提供可依赖的字节流服务
数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务

三、TCP网络编程
Java语言的基于套接字Socket编程分为客户端和服务端
基于TCP的Socket通信模型

1.基于Socke的TCP编程
1.1客户端Socket的工作过程

创建 Socket:根据指定服务端的 IP地址或端口号构造 Sσcket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。

打开连接到 Socket 的输入出流:使用 getInputstream() 方法获得输入流,使用getOutputStream() 方法获得输出流,进行数据传输

按照一定的协议对 Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程

关闭 Socket:断开客户端到服务器的连接,释放线路

说明:

客户端程序可以使用 Socket 类创建对象,创建的同时会自动向服务器方发起连接。

Socket 的构造器是:

Socket(String host,int port) throws UnknownHostException,EXCeption:向服务器(域名是 host,端口号为 port )发起 TCP 连接,若成功,则创建 Socket 对象,否则抛出异常。
Socket(InetAddress address,int port)throws IOException:根据 InetAddress 对象所表示的 IP 地址以及端口号 port 发起连接

客户端建立 socketAtClient对象的过程就是向服务器发出套接字连接请求

1.2 服务器端Socket的工作过程:

调用 ServerSocket(int port):创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。

调用 accept0():监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。

调用该 Socket 类对象的 getOutputStream() 和 getInputStream():获取输出流和输入流,开始网络数据的发送和接收。

关闭 ServerSocket 和 Socket 对象:客户端访问结束,关闭通信套接字。

说明:

ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口中的业务员。也就是说,服务器必须事先建立一个等待客户请求建立套接字连接的 Server Socket 对象。

所谓“接收”客户的套接字请求,就是 accept() 方法会返回一个 Socket 对象

1.3 代码示例
例子1:客户端发送信息给服务端,服务端将数据显示在控制台上
public class TCPTest1 {

//客户端
@Test
public void client()  {
    Socket socket = null;
    OutputStream os = null;
    try {
        //1.创建Socket对象,指明服务器端的ip和端口号
        InetAddress inet = InetAddress.getByName("192.168.14.100");
        socket = new Socket(inet,8899);
        //2.获取一个输出流,用于输出数据
        os = socket.getOutputStream();
        //3.写出数据的操作
        os.write("你好,我是客户端mm".getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源的关闭
        if(os != null){
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        if(socket != null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

//服务端
@Test
public void server()  {

    ServerSocket ss = null;
    Socket socket = null;
    InputStream is = null;
    ByteArrayOutputStream baos = null;
    try {
        //1.创建服务器端的ServerSocket,指明自己的端口号
        ss = new ServerSocket(8899);
        //2.调用accept()表示接收来自于客户端的socket
        socket = ss.accept();
        //3.获取输入流
        is = socket.getInputStream();

        //不建议这样写,可能会有乱码
        //        byte[] buffer = new byte[1024];
        //        int len;
        //        while((len = is.read(buffer)) != -1){
        //            String str = new String(buffer,0,len);
        //            System.out.print(str);
        //        }
        //4.读取输入流中的数据
        baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[5];
        int len;
        while((len = is.read(buffer)) != -1){
            baos.write(buffer,0,len);
        }

        System.out.println(baos.toString());

        System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(baos != null){
            //5.关闭资源
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(is != null){
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(socket != null){
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(ss != null){
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}
}

复制代码
例题2:客户端发送文件给服务端,服务端将文件保存在本地。
public class TCPTest2 {

/*
这里涉及到的异常,应该使用try-catch-finally处理
 */
@Test
public void client() throws IOException {
    //1.
    Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
    //2.
    OutputStream os = socket.getOutputStream();
    //3.
    FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
    //4.
    byte[] buffer = new byte[1024];
    int len;
    while((len = fis.read(buffer)) != -1){
        os.write(buffer,0,len);
    }
    //5.
    fis.close();
    os.close();
    socket.close();
}

/*
这里涉及到的异常,应该使用try-catch-finally处理
 */
@Test
public void server() throws IOException {
    //1.
    ServerSocket ss = new ServerSocket(9090);
    //2.
    Socket socket = ss.accept();
    //3.
    InputStream is = socket.getInputStream();
    //4.
    FileOutputStream fos = new FileOutputStream(new File("beauty1.jpg"));
    //5.
    byte[] buffer = new byte[1024];
    int len;
    while((len = is.read(buffer)) != -1){
        fos.write(buffer,0,len);
    }
    //6.
    fos.close();
    is.close();
    socket.close();
    ss.close();

}

}

复制代码
例题3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。
public class TCPTest3 {

/*
    这里涉及到的异常,应该使用try-catch-finally处理
     */
@Test
public void client() throws IOException {
    //1.
    Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
    //2.
    OutputStream os = socket.getOutputStream();
    //3.
    FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
    //4.
    byte[] buffer = new byte[1024];
    int len;
    while((len = fis.read(buffer)) != -1){
        os.write(buffer,0,len);
    }
    //关闭数据的输出
    socket.shutdownOutput();

    //5.接收来自于服务器端的数据,并显示到控制台上
    InputStream is = socket.getInputStream();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] bufferr = new byte[20];
    int len1;
    while((len1 = is.read(buffer)) != -1){
        baos.write(buffer,0,len1);
    }

    System.out.println(baos.toString());

    //6.
    fis.close();
    os.close();
    socket.close();
    baos.close();
}

/*
这里涉及到的异常,应该使用try-catch-finally处理
 */
@Test
public void server() throws IOException {
    //1.
    ServerSocket ss = new ServerSocket(9090);
    //2.
    Socket socket = ss.accept();
    //3.
    InputStream is = socket.getInputStream();
    //4.
    FileOutputStream fos = new FileOutputStream(new File("beauty2.jpg"));
    //5.
    byte[] buffer = new byte[1024];
    int len;
    while((len = is.read(buffer)) != -1){
        fos.write(buffer,0,len);
    }

    System.out.println("图片传输完成");

    //6.服务器端给予客户端反馈
    OutputStream os = socket.getOutputStream();
    os.write("你好,美女,照片我已收到,非常漂亮!".getBytes());

    //7.
    fos.close();
    is.close();
    socket.close();
    ss.close();
    os.close();

}

}

复制代码
四、UDP网络编程

  1. 简述

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。

UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。

DatagramPacket 对象封装了 UDP 数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号

UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。

  1. DatagramSocket 类常用方法

  2. DatagramSocket 类的使用
    流程:

DatagramSocket 与 DatagramPacket

建立发送端,接收端

建立数据包

调用 Socket 的发送、接收方法

关闭 Socket

注意:发送端与接收端是两个独立的运行程序
代码示例:
public class UDPTest {

//发送端
@Test
public void sender() throws IOException {

    DatagramSocket socket = new DatagramSocket();



    String str = "我是UDP方式发送的导弹";
    byte[] data = str.getBytes();
    InetAddress inet = InetAddress.getLocalHost();
    DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);

    socket.send(packet);

    socket.close();

}
//接收端
@Test
public void receiver() throws IOException {

    DatagramSocket socket = new DatagramSocket(9090);

    byte[] buffer = new byte[100];
    DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);

    socket.receive(packet);

    System.out.println(new String(packet.getData(),0,packet.getLength()));

    socket.close();
}

}

复制代码
五、URL编程

URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。

它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。

通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源

URL的基本结构由5部分组成: <传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
例如: http://192.168.1.100:8080/helloworld/indexjsp#a?username=shkstart&password=123
#片段名:即锚点,例如看小说,直接定位到章节
参数列表格式:参数名=参数值&参数名=参数值…

  1. URL类
    1.1 构造器
    为了表示 URL,java.net中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象

public URL(String spec):通过一个表示 URL 地址的字符串可以构造一个 URL 对象。 例如:URL url = new URL(“http://www.baidu.com/”);

public URL(URL context,String spec):通过基URL和相对URL构造一个URL对象
例如: URL downloadeUrl = new URL(url,“download.html”);

public URL(String protocol,String host,String file);
``
例如:new URL(“http”,“www.baidu.com”,80,“download.html”);

public URL(String protocol,String host,int port,String file);
例如:new URL(“http”,“www.baidu.com”,80,“download.html”);

注意:URL 类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。
1.2 方法

public String getProtocol() 获取该 URL 的协议名
public String getHost() 获取该 URL 的主机名
public String getPort() 获取该 URL 的端口号
public String getPath() 获取该 URL 的文件路径
public String getFile() 获取该 URL 的文件名
public String getQuery() 获取该 URL 的查询名

代码示例
public class URLTest {

public static void main(String[] args) {

    try {

        URL url = new URL("http://localhost:8080/examples/beauty.jpg?username=Tom");

// public String getProtocol( ) 获取该URL的协议名
System.out.println(url.getProtocol());
// public String getHost( ) 获取该URL的主机名
System.out.println(url.getHost());
// public String getPort( ) 获取该URL的端口号
System.out.println(url.getPort());
// public String getPath( ) 获取该URL的文件路径
System.out.println(url.getPath());
// public String getFile( ) 获取该URL的文件名
System.out.println(url.getFile());
// public String getQuery( ) 获取该URL的查询名
System.out.println(url.getQuery());
} catch (MalformedURLException e) {
e.printStackTrace();
}

}

}

复制代码
例子:通过URL下载
public class URLTest1 {

public static void main(String[] args) {

    HttpURLConnection urlConnection = null;
    InputStream is = null;
    FileOutputStream fos = null;
    try {
        URL url = new URL("http://localhost:8080/examples/beauty.jpg");

        urlConnection = (HttpURLConnection) url.openConnection();

        urlConnection.connect();

        is = urlConnection.getInputStream();
        fos = new FileOutputStream("beauty3.jpg");

        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }

        System.out.println("下载完成");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关闭资源
        if(is != null){
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fos != null){
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(urlConnection != null){
            urlConnection.disconnect();
        }
    }
}

}

反射

Java反射机制概述

Java Reflection

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
正常方式:引入需要的”包类”名称→通过new实例化→取得实例化对象
反射方式:实例化对象→getClass()方法→得到完整的“包类”名称

补充:动态语言 vs 静态语言

1、动态语言
是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang。
2、静态语言
与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。
Java的动态性让编程的时候更加灵活!

Java反射机制研究及应用

Java反射机制提供的功能
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时获取泛型信息
在运行时调用任意一个对象的成员变量和方法
在运行时处理注解
生成动态代理

反射相关的主要API

java.lang.Class:代表一个类
java.lang.reflect.Method:代表类的方法
java.lang.reflect.Field:代表类的成员变量
java.lang.reflect.Constructor:代表类的构造器
… …

理解Class类并获取Class的实例

Class 类

在Object类中定义了以下的方法,此方法将被所有子类继承:
● public final Class getClass()
以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
Class本身也是一个类
Class 对象只能由系统建立对象
一个加载的类在 JVM 中只会有一个Class实例
一个Class对象对应的是一个加载到JVM中的一个.class文件
每个类的实例都会记得自己是由哪个 Class 实例所生成
通过Class可以完整地得到一个类中的所有被加载的结构
Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

Class类的常用方法

方法名 功能说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型
或void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String
name,Class … paramTypes)
返回一个Method对象,此对象的形参类型为paramType

反射的应用举例

• String str = “test4.Person”;
• Class clazz = Class.forName(str);
• Object obj = clazz.newInstance();
• Field field = clazz.getField(“name”);
• field.set(obj, “Peter”);
• Object name = field.get(obj);
• System.out.println(name);
注:test4.Person是test4包下的Person类

获取Class类的实例(四种方法)

1)前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
实例:Class clazz = String.class;
2)前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
实例:Class clazz = “www.atguigu.com”.getClass();
3)前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
实例:Class clazz = Class.forName(“java.lang.String”);
4)其他方式(不做要求)
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass(“类的全类名”);

哪些类型可以有Class对象?

(1)class:
外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void

Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11);

类的加载与ClassLoader的理解

了解:类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

类的加载(Load)→类的链接(Link)→类的初始化(Initialize)

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

将类的二进制数据合并到JRE中

JVM负责对类进行初始化

加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的过程需要类加载器参与。
链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:
执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

public class ClassLoadingTest {
public static void main(String[] args) {
System.out.println(A.m);
}
}
class A {
static {
m = 300;
}
static int m = 100;
}
//第二步:链接结束后m=0
//第三步:初始化后,m的值由()方法执行决定
// 这个A的类构造器()方法由类变量的赋值和静态代码块中的语句按照顺序合并
产生,类似于
// (){
// m = 300;
// m = 100;
// }

了解:什么时候会发生类初始化?

类的主动引用(一定会发生类的初始化)
当虚拟机启动,先初始化main方法所在的类
new一个类的对象
调用类的静态成员(除了final常量)和静态方法
使用java.lang.reflect包的方法对类进行反射调用
当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用(不会发生类的初始化)
当访问一个静态域时,只有真正声明这个域的类才会被初始化
 当通过子类引用父类的静态变量,不会导致子类初始化
通过数组定义类引用,不会触发此类的初始化
引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常
量池中了)

public class ClassLoadingTest {
public static void main(String[] args) {
// 主动引用:一定会导致A和Father的初始化
// A a = new A();
// System.out.println(A.m);
// Class.forName(“com.atguigu.java2.A”);
// 被动引用
A[] array = new A[5];//不会导致A和Father的
初始化
// System.out.println(A.b);//只会初始化
Father
// System.out.println(A.M);//不会导致A和
Father的初始化
}
static {
System.out.println(“main所在的类”);
}
}
class Father {
static int b = 2;
static {
System.out.println(“父类被加载”);
}
}
class A extends Father {
static {
System.out.println(“子类被加载”);
m = 300;
}
static int m = 100;
static final int M = 1;
}

类加载器的作用:
类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方
法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为
方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器
中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

了解:ClassLoader

类加载器作用是用来把类(class)装载进内存的。JVM 规范定义了如下类型的
类的加载器。
引导类加载器:用C++编写的,是JVM自带的类
加载器,负责Java平台核心库,用来装载核心类
库。该加载器无法直接获取
扩展类加载器:负责jre/lib/ext目录下的jar包或 –
D java.ext.dirs 指定目录下的jar包装入工作库
系统类加载器:负责java –classpath 或 –D
java.class.path所指的目录下的类与jar包装入工
作 ,是最常用的加载器
15.3 类的加载与ClassLoader的理解
• //1.获取一个系统类加载器
• ClassLoader classloader = ClassLoader.getSystemClassLoader();
• System.out.println(classloader);
• //2.获取系统类加载器的父类加载器,即扩展类加载器
• classloader = classloader.getParent();
• System.out.println(classloader);
• //3.获取扩展类加载器的父类加载器,即引导类加载器
• classloader = classloader.getParent();
• System.out.println(classloader);
• //4.测试当前类由哪个类加载器进行加载
• classloader = Class.forName(“exer2.ClassloaderDemo”).getClassLoader();
• System.out.println(classloader);
15.3 类的加载与ClassLoader的理解
• //5.测试JDK提供的Object类由哪个类加载器加载
• classloader =
• Class.forName(“java.lang.Object”).getClassLoader();
• System.out.println(classloader);
• //*6.关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路
径下的指定文件的输入流
• InputStream in = null;
• in = this.getClass().getClassLoader().getResourceAsStream(“exer2\test.properties”);
• System.out.println(in);

创建运行时类的对象

有了Class对象,能做什么?

创建类的对象:调用Class对象的newInstance()方法
要 求: 1)类必须有一个无参数的构造器。
2)类的构造器的访问权限需要足够。
难道没有无参的构造器就不能创建对象了吗?
不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
步骤如下:
1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类
型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。
有了Class对象,能做什么?
以上是反射机制应用最多的地方。

//1.根据全类名获取对应的Class对象
String name = “atguigu.java.Person";
Class clazz = null;
clazz = Class.forName(name);
//2.调用指定参数结构的构造器,生成Constructor的实例
Constructor con = clazz.getConstructor(String.class,Integer.class);
//3.通过Constructor的实例创建对应类的对象,并初始化类属性
Person p2 = (Person) con.newInstance(“Peter”,20);
System.out.println(p2);

获取运行时类的完整结构

通过反射获取运行时类的完整结构

Field、Method、Constructor、Superclass、Interface、Annotation
实现的全部接口
所继承的父类
全部的构造器
全部的方法
全部的Field
通过反射获取运行时类的完整结构

使用反射可以取得:
1.实现的全部接口
public Class<?>[] getInterfaces() 确定此对象所表示的类或接口实现的接口。 2.所继承的父类 public Class<? Super T> getSuperclass() 返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。 15.5 获取运行时类的完整结构 3.全部的构造器 public Constructor[] getConstructors() 返回此 Class 对象所表示的类的所有public构造方法。 public Constructor[] getDeclaredConstructors() 返回此 Class 对象表示的类声明的所有构造方法。 Constructor类中: 取得修饰符: public int getModifiers(); 取得方法名称: public String getName(); 取得参数的类型:public Class<?>[] getParameterTypes();

4.全部的方法
public Method[] getDeclaredMethods()
返回此Class对象所表示的类或接口的全部方法
public Method[] getMethods()
返回此Class对象所表示的类或接口的public的方法
Method类中:
public Class<?> getReturnType()取得全部的返回值 public Class<?>[] getParameterTypes()取得全部的参数
public int getModifiers()取得修饰符
public Class<?>[] getExceptionTypes()取得异常信息

5.全部的Field
public Field[] getFields()
返回此Class对象所表示的类或接口的public的Field。
public Field[] getDeclaredFields()
返回此Class对象所表示的类或接口的全部Field。
Field方法中:
public int getModifiers() 以整数形式返回此Field的修饰符
public Class<?> getType() 得到Field的属性类型
public String getName() 返回Field的名称。

  1. Annotation相关
    get Annotation(Class annotationClass)
    getDeclaredAnnotations()
    7.泛型相关
    获取父类泛型类型:Type getGenericSuperclass()
    泛型类型:ParameterizedType
    获取实际的泛型类型参数数组:getActualTypeArguments()
    8.类所在的包 Package getPackage()

小 结:
1.在实际的操作中,取得类的信息的操作代码,并不会经常开发。
2.一定要熟悉java.lang.reflect包的作用,反射机制。
3.如何取得属性、方法、构造器的名称,修饰符等。

调用运行时类的指定结构

1.调用指定方法

通过反射,调用类中的方法,通过Method类完成。步骤:
1.通过Class类的getMethod(String name,Class…parameterTypes)方法取得
一个Method对象,并设置此方法操作时所需要的参数类型。
2.之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中
传递要设置的obj对象的参数信息。
Class.forName()
getMethod(“sayHello”)
invoke()
1.实例化Class
2.找到sayHello()
3.调用方法
Person
+sayHello():void

Object invoke(Object obj, Object … args)
说明:
1.Object 对应原方法的返回值,若原方法无返回值,此时返回null
2.若原方法若为静态方法,此时形参Object obj可为null
3.若原方法形参列表为空,则Object[] args为null
4.若原方法声明为private,则需要在调用此invoke()方法前,显式调用
方法对象的setAccessible(true)方法,将可访问private的方法。

2.调用指定属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和
get()方法就可以完成设置和取得属性内容的操作。
public Field getField(String name) 返回此Class对象表示的类或接口的指定的
public的Field。
public Field getDeclaredField(String name)返回此Class对象表示的类或接口的
指定的Field。
在Field中:
public Object get(Object obj) 取得指定对象obj上此Field的属性内容
public void set(Object obj,Object value) 设置指定对象obj上此Field的属性内容

关于setAccessible方法的使用

Method和Field、Constructor对象都有setAccessible()方法。
setAccessible启动和禁用访问安全检查的开关。
参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被
调用,那么请设置为true。
使得原本无法访问的私有成员也可以访问
参数值为false则指示反射的对象应该实施Java语言访问检查。

反射的应用:动态代理

代理设计模式的原理:

使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
之前为大家讲解过代理机制的操作,属于静态代理,特征是代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时,每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。最好可以通过一个代理类完成全部的代理功能。

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
动态代理使用场合:
调试
远程方法调用
动态代理相比于静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。

Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一
个或多个接口动态地生成实现类。
提供用于创建动态代理类和动态代理对象的静态方法
static Class<?> getProxyClass(ClassLoader loader, Class<?>… interfaces) 创建
一个动态代理类所对应的Class对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
InvocationHandler h) 直接创建一个动态代理对象
Java动态代理相关API
类加载器 得到被代理类实
现的全部接口
得到InvocationHandler接
口的实现类实例

动态代理步骤

1.创建一个实现接口InvocationHandler的类,它必须实现invoke方
法,以完成代理的具体操作。
public Object invoke(Object theProxy, Method method, Object[] params)
throws Throwable{
try{
Object retval = method.invoke(targetObj, params);
// Print out the result
System.out.println(retval);
return retval;
}catch (Exception exc){}
}
代理类的对象
要调用的方法
方法调用时所
需要的参数

2.创建被代理的类以及接口
Subject RealSubject implements
say(String name,int age)

3.通过Proxy的静态方法
newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建
一个Subject接口代理
RealSubject target = new RealSubject();
// Create a proxy to wrap the original implementation
DebugProxy proxy = new DebugProxy(target);
// Get a reference to the proxy through the Subject interface
Subject sub = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(),new Class[] { Subject.class }, proxy);

4.通过 Subject代理调用RealSubject实现类的方法
String info = sub.say(“Peter", 24);
System.out.println(info);

动态代理与AOP(Aspect Orient Programming)

前面介绍的Proxy和InvocationHandler,很难看出这种动态代理的优势,下
面介绍一种更实用的动态代理机制
相同的代
码段
相同的代
码段
相同的代
码段
代码段2
代码段3
代码段1
通过复制、
粘贴的部分

调用方法
调用方法
调用方法
代码段2
代码段3
代码段1
相同的代
码段
方法A
改进后的说明:代码段1、代码段2、代码段3和深色代码段分离开了,但代码段1、2、3又和一个特定的方法A耦合了!最理想的效果是:代码块1、2、3既可以执行方法A,又无须在程序中以硬编码的方式直接调用深色代码的方法

public interface Dog{
void info();
void run();
}
public class HuntingDog implements Dog{
public void info(){
System.out.println(“我是一只猎狗”);
}
public void run(){
System.out.println(“我奔跑迅速”);
}
}

动态代理与AOP(Aspect Orient Programming)
public class DogUtil{
public void method1(){
System.out.println(“=模拟通用方法一=”);
}
public void method2(){
System.out.println(“=模拟通用方法二=”);
}
}

public class MyInvocationHandler implements InvocationHandler{
// 需要被代理的对象
private Object target;
public void setTarget(Object target){
this.target = target;}
// 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception{
DogUtil du = new DogUtil();
// 执行DogUtil对象中的method1。
du.method1();
// 以target作为主调来执行method方法
Object result = method.invoke(target , args);
// 执行DogUtil对象中的method2。
du.method2();
return result;}}

public class MyProxyFactory{
// 为指定target生成动态代理对象
public static Object getProxy(Object target)
throws Exception{
// 创建一个MyInvokationHandler对象
MyInvokationHandler handler =
new MyInvokationHandler();
// 为MyInvokationHandler设置target对象
handler.setTarget(target);
// 创建、并返回一个动态代理对象
return
Proxy.newProxyInstance(target.getClass().getClassLoader()
, target.getClass().getInterfaces() , handler);
}
}

public class Test{
public static void main(String[] args)
throws Exception{
// 创建一个原始的HuntingDog对象,作为target
Dog target = new HuntingDog();
// 以指定的target来创建动态代理
Dog dog = (Dog)MyProxyFactory.getProxy(target);
dog.info();
dog.run();
}
}

使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理
这种动态代理在AOP中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理

动态代理增加的通用方法1
回调目标对象的方法
动态代理增加的通用方法2
一、反射的概述

  1. 反射的简介

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

通常的方式:引入需要的“包类”名称---->通过 new 实例化---->获得实例化对象
反射的方式:实例化对象----> getClass() 方法---->得到完整的“包类”名称
框架 = 注解 + 反射 + 设计模式
2. 反射动态性体现
只有当程序运行时我们才能知道调用的类
@Test
public void test2(){

for(int i = 0;i < 100;i++){
    int num = new Random().nextInt(3);//0,1,2
    String classPath = "";
    switch(num){
        case 0:
            classPath = "java.util.Date";
            break;
        case 1:
            classPath = "java.lang.Object";
            break;
        case 2:
            classPath = "com.atguigu.java.Person";
            break;
    }

    try {
        Object obj = getInstance(classPath);
        System.out.println(obj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}

/*
创建一个指定类的对象。
classPath:指定类的全类名
*/
public Object getInstance(String classPath) throws Exception {
Class clazz = Class.forName(classPath);
return clazz.newInstance();
}

复制代码
3. 反射机制提供的功能

在运行时判断任意一个对象所属的类

在运行时构造任意一个类的对象

在运行时判断任意一个类所具有的成员变量和方法

在运行时获取泛型信息

在运行时调用任意一个对象的成员变量和方法

在运行时处理注解

生成动态代理

代码示例
@Test
public void test1() throws Exception {
Class clazz = Person.class;
//1.通过反射,创建Person类对象
Constructor cons = clazz.getConstructor(String.class, int.class);
Person person = cons.newInstance(“Tom”, 12);
System.out.println(person);//Person{name=‘Tom’, age=12}

//2.通过反射,调用对象指定的属性、方法
//调用属性
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
age.set(person, 10);
System.out.println(person.toString());//Person{name='Tom', age=10}

//调用方法
Method show = clazz.getDeclaredMethod("show");
show.invoke(person);//my name is Tom and age is 10

System.out.println("===================================");
//通过反射,可以调用Person类的私有结构的。比如:私有的构造器、方法、属性
//调用私有的构造器
Constructor<Person> cons1 = clazz.getDeclaredConstructor(String.class);
cons1.setAccessible(true);
Person p1 = cons1.newInstance("Bruce");
System.out.println(p1);//Person{name='Bruce', age=0}

//调用私有的属性
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
name.set(p1, "Jarry");
System.out.println(p1);

//调用私有的方法
Method nation = clazz.getDeclaredMethod("nation", String.class);
nation.setAccessible(true);
Object nation1 = (String) nation.invoke(p1, "China");//相当于String nation = p1.showNation("China")
System.out.println(nation1);//I come from China

}

复制代码
4. 相关API

java.lang.Class:反射的源头
java.lang.reflect.Method:反射方法
java.lang.reflect.Field:反射参数
java.lang.reflect.Constructor:反射构造器

二、Class类

  1. Class简述

在 Object 类中定义了以下的方法,此方法将被所有子类继承:

public final Class getClass()

以上的方法返回值的类型是一个 Class 类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即可以通过对象反射求出类的名称。

对象使用反射后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构( class/interface/enum/annotation/primitive type/void/[])的有关信息。

Class 本身也是一个类
Class 对象只能由系统建立对象
一个加载的类在 JVM 中只会有一个 Class 实例
一个 Class 对象对应的是一个加载到 JVM 中的一个 .class 文件
每个类的实例都会记得自己是由哪个 Class 实例所生成
通过 Class 可以完整地得到一个类中的所有被加载的结构
Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的 Class 对象

类的加载过程:

程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class 结尾)。接着我们使用 java.exe 命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为 Class 的一个实例。
换句话说,Class 的实例就对应着一个运行时类。
加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。

  1. Class类的常用方法

代码示例:
//创建Class的实例
String str = “test1.Person”;
Class clazz = Class.forName(str);
//调用Class的空参构造器创建对象
Object obj = clazz.newInstance;
//获取clazz的name属性
Field field = clazz.getField(“name”);
field.set(obj,“Jarry”);

Object name = filed.get(obj);
System.out.println(name);
//test1.Person为test1包下的Person类

复制代码
3. 获取Class实例的几种方式:
1)已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高 实例:Class clazz = String.class;
2)已知某个类的实例,调用该实例的 getclass() 方法获取 Class 对象 实例:Class clazz=person.getclass();
3)已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName() 获取,可能抛出 ClassNotFoundException(比较常用)
实例:Class clazz = Class.forName(String classPath)
4)通过类加载器 ClassLoader cl = this.getclass().getClassLoader(); Class clazz = cl.loadClass(“类的全类名”);
代码示例
@Test
public void test2() throws ClassNotFoundException {
//方式一:调用运行时类的属性:.class
Class clazz1 = Person.class;
System.out.println(clazz1);//class cn.bruce.java.Person

//方式二:通过运行时类的对象,调用getClass()
Person p1 = new Person();
Class<? extends Person> clazz2 = p1.getClass();
System.out.println(clazz2);//class cn.bruce.java.Person

//方式三:调用Class的静态方法:forName(String classPath)
Class<?> clazz3 = Class.forName("cn.bruce.java.Person");
System.out.println(clazz3);//class cn.bruce.java.Person

System.out.println(clazz1 == clazz2);//true
System.out.println(clazz1 == clazz3);//true
//方式四:使用类的加载器:ClassLoader  (了解)
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
Class<?> clazz4 = classLoader.loadClass("cn.bruce.java.Person");
System.out.println(clazz4);//class cn.bruce.java.Person
System.out.println(clazz1 == clazz4);//true

}

复制代码
总结:创建类的对象的方式?
方式一:new + 构造器
方式二:要创建Xxx类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在。可以调用其静态方法,创建Xxx对象。
方式三:通过反射
4. Class实例可以代表的结构
(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void:无返回值

在Java中万事万物皆对象

代码示例
@Test
public void test3(){
Class c1 = Object.class;
Class c2 = Comparable.class;
Class<String[]> c3 = String[].class;
Class<int[][]> c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;

int[] i1 = new int[10];
int[] i2 = new int[100];
Class<? extends int[]> c10 = i1.getClass();
Class<? extends int[]> c11 = i2.getClass();
// 只要数组的元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11);//true

}

复制代码
三、类的加载

  1. 类的加载过程
    当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过以下三个步骤对该类进行初始化。

加载:将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个 Class 对象。这个加载的过程需要类加载器参与。
链接:将 Java 类的二进制代码合并到JVM的运行状态之中的过程。

验证:确保加载的类信息符合JVM规范,例如:以 cafe 开头,没有安全方面的问题。
准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

初始化:

执行类构造器 () 方法的过程。类构造器 () 方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和同步。

代码示例:
public class ClassLoadingTest{
public static void main (String [] args){
System.out.println(test.m);
}
}

class test{
static {
m = 300;
}
static int m = 100;
}
//第一步:加载
//第二步:链接结束后m=0
//第三步:初始化结束后,m的值由()方法执行决定
/*
这个test构造器()方法由类变量的赋值和静态代码块中的语句按照顺序合并产生,类似于
(){
m = 300;
m = 100;
}
*/

复制代码
2. Java类编译、运行的执行的流程

  1. 类的加载器的作用

将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口。

类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些 Class 对象

  1. 类的加载器的分类

@Test
public void test1(){
//对于自定义类,使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);
//调用系统类加载器的getParent():获取扩展类加载器
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);
//调用扩展类加载器的getParent():无法获取引导类加载器
//引导类加载器主要负责加载java的核心类库,无法加载自定义类的。
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);

ClassLoader classLoader3 = String.class.getClassLoader();
System.out.println(classLoader3);

}

复制代码
5. 使用Classloader加载src目录下的配置文件
@Test
public void test3(){
Properties pros = new Properties();
// //读取配置文件的方式一:
// //此时的文件默认在当前的module下。
// FileInputStream fis = null;
// try {
// fis = new FileInputStream(“jdbc1.properties”);
// pros.load(fis);
// } catch (IOException e) {
// e.printStackTrace();
// } finally {
// if (fis != null) {
// try {
// fis.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }

//读取配置文件的方式二:使用ClassLoader
//配置文件默认识别为:当前module的src下
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
try {
    pros.load(is);
} catch (IOException e) {
    e.printStackTrace();
}

String user = pros.getProperty("user");
String password = pros.getProperty("password");
System.out.println("user = " + user + " password =" + password);

}

复制代码
四、反射的应用

  1. 创建运行时类的对象
    1.1代码举例
    @Test
    public void test1() throws Exception {
    //方式一
    Class clazz1 = Person.class;
    //方式二
    Class clazz2 = (Class) Class.forName(“cn.bruce.java.Person”);

    Person person1 = clazz1.newInstance();
    Person person2 = clazz2.newInstance();
    System.out.println(person1);
    System.out.println(person2);

}

复制代码
1.2 说明
newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
要想此方法正常的创建运行时类的对象,要求:

运行时类必须提供空参的构造器

空参的构造器的访问权限得够。通常,设置为 public。

在 javabean 中要求提供一个 public 的空参构造器。原因:

便于通过反射,创建运行时类的对象

便于子类继承此运行时类时,默认调用 super() 时,保证父类此构造器

  1. 获取运行时类的完整结构
    我们可以通过反射,获取对应的运行时类中所有的属性、方法、构造器、父类、接口、父类的泛型、包、注解、异常等。。。。
    2.1 使用反射可以取得:

实现的全部接口: public Class<?>[] getInterfaces() 确定此对象所表示的类或接口实现的接口。

所继承的父类: public Class<? Super T> getSuperclass() 返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

全部的构造器:
public Constructor[] getConstructors()
返回此 Class 对象所表示的类的所有 public 构造方法。
public Constructor[] getDeclaredConstructors()
返回此Class对象表示的类声明的所有构造方法。
在Constructor类中:

取得修饰符:public int getModifiers();
取得方法名称: public String getName();
取得参数的类型: public Class<?> getParameterTypes();

全部的方法:
public Method[] getDeclaredMethods()
返回此Class对象所表示的类或接口的全部方法
public Method[] getMethods()
返回此 Class 对象所表示的类或接口的 public 的方法
Method 类中:

public Class<?> getReturnType():取得全部的返回值 public Class<?>[] getParameterTypes():取得全部的参数
public int getModifiers():取得修饰符
public Class<?> [] getEXceptionTypes():取得异常信息

全部的 Field:
public Field[] getFields()
返回此 Class 对象所表示的类或接口的 public 的 Field。
public Field[] getDeclaredFields()
返回此 Class 对象所表示的类或接口的全部 Field
Field 方法中

public int getModifiers():以整数形式返回此 Field 的修饰符
public Class<?> getType():得到 Field 的属性类型
public String getName():返回 Field 的名称。

Annotation 相关
get Annotation(Class annotationClass)
getDeclaredAnnotations()

泛型相关
获取父类泛型类型:Type getGenericSuperclass()
泛型类型:ParameterizedType
获取实际的泛型类型参数数组:getActualTypeArguments()

类所在的包 Package getPackage()

2.2 代码示例
获取属性 Fieled
public class FiledTest {
@Test
public void test1() {
Class clazz = Person.class;
//获取属性结构
//getFields():获取当前运行时类及其父类中声明为public访问权限的属性
Field[] fields = clazz.getFields();
for (Field f :
fields) {
System.out.println(f);
}
System.out.println();
//getDeclaredFields():获取当前运行时类中声明的所有属性。(不包含父类中声明的属性)
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f :
declaredFields) {
System.out.println(f);
}
}

//权限修饰符  数据类型 变量名
@Test
public void test2() throws ClassNotFoundException {
    Class<?> clazz = Class.forName("cn.bruce.java1.Person");
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field f :
         declaredFields) {
        //1.权限修饰符
        int modifiers = f.getModifiers();
        System.out.print(Modifier.toString(modifiers)+"\t");

        //2.数据类型
        Class<?> type = f.getType();
        System.out.print(type.getName()+"\t");

        //3.变量名
        String fName = f.getName();
        System.out.print(fName);

        System.out.println();
    }
}

}

复制代码
获取方法 Method
public class MethodTest {
@Test
public void test1() {
Class clazz = Person.class;
//getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
Method[] methods = clazz.getMethods();
for (Method m :
methods) {
System.out.println(m);
}
System.out.println(“============”);
//getDeclaredMethods():获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m :
declaredMethods) {
System.out.println(m);
}
}

/*
@Xxxx
权限修饰符  返回值类型  方法名(参数类型1 形参名1,...) throws XxxException{}
 */
@Test
public void test2() throws ClassNotFoundException {
    Class<?> clazz = Class.forName("cn.bruce.java1.Person");
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method m :
         declaredMethods) {
        //1.获取方法声明的注解
        Annotation[] annos = m.getAnnotations();
        for (Annotation a :
             annos) {
            System.out.println(a);
        }

        //2.权限修饰符
        System.out.print(Modifier.toString(m.getModifiers())+"\t");

        //3.返回值类型
        System.out.print(m.getReturnType().getName() + "\t");

        //4.方法名
        System.out.print(m.getName());
        System.out.print("(");

        //5.形参列表
        Class<?>[] parameterTypes = m.getParameterTypes();
        if (!(parameterTypes == null && parameterTypes.length == 0)) {
            for (int i = 0; i < parameterTypes.length; i++) {
                if (i == parameterTypes.length - 1) {
                    System.out.print(parameterTypes[i].getName() + " args_" + i);
                    break;
                }
                System.out.print(parameterTypes[i].getName() + "args_" + i + ",");
            }
        }
        System.out.print(")");

        //6.抛出的异常
        Class<?>[] exceptionTypes = m.getExceptionTypes();
        if (exceptionTypes.length > 0){
            System.out.print("throws ");
            for (int i = 0; i < exceptionTypes.length; i++) {
                if (i==exceptionTypes.length -1){
                    System.out.print(exceptionTypes[i].getName());
                    break;
                }
                System.out.print(exceptionTypes[i].getName()+",");
            }

            System.out.println();
        }


    }
}

}

复制代码
获取其他结构
public class OtherTest {
/*
获取构造器结构
/
@Test
public void test1() {
Class clazz = Person.class;
//getConstructors():获取当前运行时类中声明为public的构造器
Constructor<?>[] constructors = clazz.getConstructors(); for (Constructor c : constructors) { System.out.println(c); } System.out.println("================"); //getDeclaredConstructors():获取当前运行时类中声明的所有的构造器 Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor c :
declaredConstructors) {
System.out.println©;
}
}
/

获取运行时类的父类
*/
@Test
public void test2(){
Class clazz = Person.class;

    Class<? super Person> superclass = clazz.getSuperclass();
    System.out.println(superclass);
}
/*
获取运行时类的带泛型的父类
 */
@Test
public void test3(){
    Class<Person> clazz = Person.class;

    Type genericSuperclass = clazz.getGenericSuperclass();
    System.out.println(genericSuperclass);
}
/*
获取运行时类的带泛型的父类的泛型
代码:逻辑性代码  vs 功能性代码
 */
@Test
public void test4(){
    Class clazz = Person.class;

    Type genericSuperclass = clazz.getGenericSuperclass();
    ParameterizedType paramType = (ParameterizedType) genericSuperclass;
    //获取泛型类型
    Type[] actualTypeArguments = paramType.getActualTypeArguments();
    //        System.out.println(actualTypeArguments[0].getTypeName());
    System.out.println(((Class)actualTypeArguments[0]).getName());
}

/*
获取运行时类实现的接口
 */
@Test
public void test5(){
    Class clazz = Person.class;

    Class[] interfaces = clazz.getInterfaces();
    for(Class c : interfaces){
        System.out.println(c);
    }

    System.out.println();
    //获取运行时类的父类实现的接口
    Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
    for(Class c : interfaces1){
        System.out.println(c);
    }

}
/*
    获取运行时类所在的包
 */
@Test
public void test6(){
    Class clazz = Person.class;

    Package pack = clazz.getPackage();
    System.out.println(pack);
}

/*
    获取运行时类声明的注解
 */
@Test
public void test7(){
    Class clazz = Person.class;

    Annotation[] annotations = clazz.getAnnotations();
    for(Annotation annos : annotations){
        System.out.println(annos);
    }
}

}

复制代码
3. 调用运行时类的指定结构
3.1 调用指定的属性
在反射机制中,可以直接通过 Field 类操作类中的属性,通过 Field 类提供的 set() 和get() 方法就可以完成设置和取得属性内容的操作。

public Field getField(String name) 返回此 Class 对象表示的类或接口的指定的 public 的 Field。

public Field getDeclaredField(String name) 返回此 Class 对象表示的类或接口的指定的 Field。
在 Field 中:

public Object get(object obj) 取得指定对象 obj 上此 Field 的属性内容

public void set(Object obj,Object value) 设置指定对象 obj 上此 Field 的属性内容

代码示例
@Test
public void testField() throws Exception {
Class clazz = Person.class;

//创建运行时类的对象
Person p = (Person) clazz.newInstance();

//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");

//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(p,"Tom");

System.out.println(name.get(p));

}

复制代码
3.2调用指定的方法(常用)
通过反射,调用类中的方法,通过 Method 类完成。步骤:

通过 Class 类的 getMethod(String name,Class… parameterTypes) 方法取得一个 Method 对象,并设置此方法操作时所需要的参数类型。

之后使用 Object invoke(Object obj, Object[] args) 进行调用,并向方法中传递要设置的 obj 对象的参数信息。

Object invoke(object obj,Object… args)方法:

Object 对应原方法的返回值,若原方法无返回值,此时返回 null

若原方法若为静态方法,此时形参 Object obj 可为 null

若原方法形参列表为空,则 Object[] args 为 null

若原方法声明为 private,则需要在调用此 invoke() 方法前,显式调用方法对象的 setAccessible(true) 方法,将可访问 private 的方法。

关于 setAccessible 方法的使用:

Method 和 Field、Constructor 对象都有 setAccessible() 方法。

setAccessible 是启动和禁用访问安全检查的开关

参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检査。

提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true. 使得原本无法访问的私有成员也可以访问

参数值为 false 则指示反射的对象应该实施 Java 语言访问检査。

代码示例
@Test
public void testMethod() throws Exception {
Class clazz = Person.class;

//创建运行时类的对象
Person person = clazz.newInstance();

/*
    1.获取指定的某个方法
    getDeclaredMethod():参数1 :指明获取的方法的名称  参数2:指明获取的方法的形参列表
     */
Method show = clazz.getDeclaredMethod("show", String.class);

//2.保证当前方法是可访问的
show.setAccessible(true);

/*
    3. 调用方法的invoke():参数1:方法的调用者  参数2:给方法形参赋值的实参
    invoke()的返回值即为对应类中调用的方法的返回值。
     */
Object returnValue = show.invoke(person, "CHN");
System.out.println(returnValue);

System.out.println("*************如何调用静态方法*****************");

Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//如果调用的运行时类中的方法没有返回值,则此invoke()返回null
//Object returnVal = showDesc.invoke(null);
Object returnVal = showDesc.invoke(Person.class);
System.out.println(returnVal);

}

复制代码
3.3 调用指定的构造器
代码示例
@Test
public void testConstructor() throws Exception {
Class clazz = Person.class;

//private Person(String name)
/*
    1.获取指定的构造器
    getDeclaredConstructor():参数:指明构造器的参数列表
     */

Constructor constructor = clazz.getDeclaredConstructor(String.class);

//2.保证此构造器是可访问的
constructor.setAccessible(true);

//3.调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");
System.out.println(per);

}

复制代码
4. 动态代理
4.1代理设计模式的原理:
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
4.2 静态代理
代码示例:
interface ClothFactory{
void produceCloth();
}

//被代理类
class NikeClothFactory implements ClothFactory{

@Override
public void produceCloth() {
    System.out.println("Nike 生产衣服");
}

}

//代理类
class ProxyClothFactory implements ClothFactory{

private ClothFactory factory;//用被代理类对象进行实例化

public ProxyClothFactory(ClothFactory factory) {
    this.factory = factory;
}

@Override
public void produceCloth() {
    System.out.println("代理工厂做一些准备工作");

    factory.produceCloth();

    System.out.println("代理工厂做一些后续的收尾工作");

}

}

//测试
public class StaticProxyTest {
public static void main(String[] args) {

    //创建被代理类的对象
    ClothFactory nike = new NikeClothFactory();

    //创建代理类的对象
    ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);

    proxyClothFactory.produceCloth();
}

}

复制代码
静态代理的缺点:
① 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。
② 每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。
4.3 动态代理的特点:
动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
相比于静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
4.4 动态代理的实现
4.4.1 需要解决的两个主要问题:
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
(通过 Proxy.newProxyInstance() 实现)
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法。
(通过 InvocationHandler 接口的实现类及其方法 invoke() )
4.4.2 动态代理相关的API:
Proxy:专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。提供用于创建动态代理类和动态代理对象的静态方法。

static Class<?> getProxyClass(ClassLoader loader, Class<?>…interface) 创建一个动态代理类所对应的 Class 对象
static Object newProxyInstance(ClassLoader loader, Class<?>…interface, InvocationHandler h) 直接创建一个动态代理对象

4.4.3 动态代理实现步骤:

创建一个实现接口 InvocationHandler 的类,它必须实现invoke方法,以完成代理的具体操作。

创建被代理类以及接口

通过Proxy的静态方法 newProxyInstance(ClassLoader loader, Class<?>…interface, InvocationHandler h) 创建一个接口代理

通过代理类的实例调用被代理类的方法

4.4.2 代码实现:
interface Human {
String getBelief();

void eat(String food);

}

//被代理类
class SuperMan implements Human {

@Override
public String getBelief() {
    return "I believe I can fly!";
}

@Override
public void eat(String food) {
    System.out.println("I like eat " + food);
}

}

/*
要想实现动态代理,需要解决的问题?
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。
*/

//创建继承了InvocationHandler接口的类
class MyInvocationHanlder implements InvocationHandler {
private Object obj;//需要使用被代理类的对象进行赋值

public void bind(Object obj) {
    this.obj = obj;
}
//当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
//将被代理类要执行的方法a的功能就声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
    //obj:被代理类的对象
    Object returnValue = method.invoke(obj, args);

    //上述方法的返回值就作为当前类中的invoke()的返回值。
    return returnValue;
}

}

class ProxyFactory {
//调用此方法,返回一个代理类的对象。解决问题一
public static Object getProxyInstance(Object obj) {
MyInvocationHanlder hanlder = new MyInvocationHanlder();
hanlder.bind(obj);
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),hanlder);

}

}

//测试动态代理
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//proxyInstance:代理类的对象
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
//当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat(“火锅”);
}
}

复制代码
5. 动态代理与AOP

使用 Proxy 生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理。

这种动态代理在 AOP 中被称为 AOP 代理,AOP 代理可代替目标对象,AOP 代理包含了目标对象的全部方法。但 AOP 代理中的方法与目标对象的方法存在差异:

AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理

代码示例
//公共接口
interface Dog {
void info();

void run();

}

//被代理类
class HuntingDog implements Dog {

@Override
public void info() {
    System.out.println("我是一只猎狗");
}

@Override
public void run() {
    System.out.println("我跑的很快");
}

}

//通用方法
class DogUtils {
public void method1() {
System.out.println(“=通用方法一=”);
}

public void method2() {
    System.out.println("=======通用方法二=======");
}

}

//动态代理实现
class MyInvocationHandler1 implements InvocationHandler {
//需要被代理的对象
private Object target;

public void SetTarget(Object target) {
    this.target = target;
}

//当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
//将被代理类要执行的方法a的功能就声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    DogUtils dogUtils = new DogUtils();
    //执行DogUtils对象中的method1
    dogUtils.method1();
    //通过obj对象来调用执行method方法
    Object result = method.invoke(target, args);
    //执行DogUtils对象中的method2
    dogUtils.method2();
    return result;
}

}

//动态代理类
class MyProxyFactory1 {
//为target生成动态代理对象
public static Object getProxy(Object target) {
//创建一个MyInvocationHandler对象
MyInvocationHandler1 handler = new MyInvocationHandler1();
//为MyInvocationHandler设置target对象
handler.SetTarget(target);
//创建返回一个动态代理对象
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
}
}

public class AOPTest {
public static void main(String[] args) {
Dog target = new HuntingDog();
Dog dog = (Dog) MyProxyFactory1.getProxy(target);
dog.info();
dog.run();
}
}

作者:RealPluto
链接:https://juejin.cn/post/6962021539914973214
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

多线程

  • 程序、进程、线程
  • 程序(program)
    概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
  • 进程(process)
    概念:程序的一次执行过程,或是正在运行的一个程序。
    说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程(thread)
    概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
    说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
  • 内存结构

类加载器加载.class文件
虚拟机栈:存放方法、变量的地方
本地方法栈:存放native方法的地方
程序计数器:每个线程有一份,栈也是
方法区:static的在方法区
堆:new的对象在堆空间,
进程可以细化为多个线程。 每个线程,拥有自己独立的:栈、程序计数器,多个线程,共享同一个进程中的结构:方法区、堆。

  • 单核CPU与多核CPU

单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。涉及到CPU处理线程的方式,CPU在单位时间(也就是说一个时间片内)内只能处理一个线程,于是就将其他的线程设置为阻塞状态,加入到阻塞队列中,等到处理完成当前线程后从就绪队列中取出新的线程进行处理,由于切换和处理时间很快用户感知不到于是用户便认为CPU在同一时间内处理多个线程。

多核CPU,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
“单核多线程:一个人同时做多个事;多核多线程:多个人同时做多个事”

  • 并行与并发的理解

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

  • 多线程程序的优点

提高应用程序的响应。对图形化界面更有意义,可增强用户体验,比如同时点击多个东西。
提高计算机系统CPU的利用率。
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

  • 应用的场景

程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
需要一些后台运行的程序时

线程的创建

  • Thread类

Java语言的JVM允许程序运行多个线程,它通过 java. lang.Thread类来体现

  • Thread类的特性
    每个线程都是通过某个特定 Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体 通过该 Thread对象的 start()方法来启动这个线程,而非直接调用run
  • 构造器
    Thread():创建新的 Thread对象
    Thread(String threadName):创建线程并指定线程实例名
    Thread(Runnable target):指定创建线程的目标对象,它实现了
    Runnable接口中的run方法
    Thread(Runnable target, String name):创建新的 Thread对象

继承Thread类创建线程

  • 创建一个继承于Thread类的子类
  • 重写Thread类的run() --> 将此线程执行的操作声明在run()中
  • 创建Thread类的子类的对象
  • 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()

注意点

  • 我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().(注意后面的点)
  • 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
    run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  • 想要启动多线程,必须调用start方法。
  • 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“lllegalThreadStateException”.
//1.继承Thread类
class MyThread extends Thread {
    public MyThread() {
    }
    //2.重run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
    	//3.新建Thread对象
        MyThread myThread = new MyThread();
        //4.调用start方法
        myThread.start();
    }
}

实现Runnable接口创建线程

  • 创建一个实现了Runnable接口的类
  • 实现类去实现Runnable中的抽象方法:run()
  • 创建实现类的对象
  • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类
public class RunnableTest implements Runnable {
    // 2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

class test {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        RunnableTest runnableTest = new RunnableTest();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread thread = new Thread(runnableTest);
        //5. 通过Thread类的对象调用start()
        thread.start();
    }
}
  • 两种方式的对比
  • 开发中优先选择:实现Runnable接口的方式
    • 实现的方式没类的单继承性的局限性
    • 实现的方式更适合来处理多个线程共享数据的情况。
  • 联系:public class Thread implements Runnable
  • 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。 目前两种方式,要想启动线程,都是调用的Thread类中的start()。

采用匿名类的方式创建线程

public class ThreadDemo {
    public static void main(String[] args) {
        //创建Thread类的匿名子类的方式
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}
  • Thread类的常用方法

start():启动当前线程;调用当前线程的run(),只有Thread类和他的子类才能调用start()方法
run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():释放当前cpu的执行权
join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
stop():已过时。当执行此方法时,强制结束当前线程。
sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
isAlive():判断当前线程是否存活

  • 线程的优先级

MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级

  • 获取和设置当前线程的优先级:
    getPriority():获取线程的优先级
    setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程CPU的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

  • 线程通信

wait()/notify()/notifyAll() :此三个方法定义在Object类中的。

  • 线程的分类

守护线程,如:垃圾回收线程,依赖于主线程而存在
用户线程,如:main方法的线程

  • Thread的生命周期
  • 线程的五种状态
    • 新建:当一个 Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    • 就绪:处于新建状态的线程被star()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
    • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
    • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
    • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

说明:
生命周期关注两个概念:状态、相应的方法
关注:状态a–>状态b:哪些方法执行了(回调方法) 某个方法主动调用:状态a–>状态b
阻塞:临时状态,不可以作为最终状态
死亡:最终状态。

线程的同步

例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式

问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

使用同步代码块

synchronized(同步监视器){//同步监视器就是需要同步线程的公共对象
   //需要被同步的代码 
}

说明:

操作共享数据的代码,即为需要被同步的代码。 不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求多个线程必须要共用同一把锁。

在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

继承Runnable接口形式同步代码块

public class Ticket implements Runnable {
    private int tick = 100;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "号窗口买票,票号为:" + tick--);
                } else {
                    break;
                }
            }
        }
    }
}

class TicketTest {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        Thread thread3 = new Thread(ticket);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

继承Thread类形式同步代码块

public class Ticket2 extends Thread {
    private static int tick = 100;
    private static Object object = new Object();

    public Ticket2() {
    }
    @Override
    public void run() {
        while (true) {
            synchronized (object) {
            //synchronized (Ticket2.class) {//通过反射调用当前类
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "号窗口买票,票号为" + tick--);
                } else {
                    break;
                }
            }

        }
    }
}

class TicketTest2 {
    public static void main(String[] args) {
        Ticket2 ticket1 = new Ticket2();
        Ticket2 ticket2 = new Ticket2();
        Ticket2 ticket3 = new Ticket2();

        ticket1.setName("窗口1");
        ticket2.setName("窗口2");
        ticket3.setName("窗口3");

        ticket1.start();
        ticket2.start();
        ticket3.start();
    }
}

使用同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

public synchronized void show(String namer){
	....
}
public class Ticket3 implements Runnable {
    private int tick = 100;
    private boolean isFlag = true;
    @Override
    public void run() {
        while (isFlag) {
            show();
        }
    }
    public synchronized void show() {//同步show方法,继承Thread类方法一样,只需同步方法即可,同时需要给方法加static关键字,确保不会创建多个对象
        if (tick > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "号窗口买票,票号为:" + tick--);
        } else {
            isFlag = false;
        }
    }
}
class TicketTest3 {
    public static void main(String[] args) {
        Ticket3 ticket = new Ticket3();

        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        Thread thread3 = new Thread(ticket);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

使用Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 Reentrantlock,可以显式加锁、释放锁。

class A {
    //1.实例化ReentrantLock对象
    private final ReenTrantLock lock = new ReenTrantLook();
    public void m (){
        lock.lock//2.先加锁
        try{
            //保证线程同步的代码
        }finally{
            lock.unlock();//3.后解锁
        }
    }
}
//注意:如果同步代码块有异常,要将unlock()写入finally语句块中

代码示例

class Window implements Runnable{
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{
                //2.调用锁定方法lock()
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
          			System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 同步方法的总结:
    在《 Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他仼务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
    synchronized的锁是什么:

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class

注意点:

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身

  1. 同步的范围:
    如何找问题,即代码是否存在线程安全?(非常重要)
    (1)明确哪些代码是多线程运行的代码
    (2)明确多个线程是否有共享数据
    (3)明确多线程运行代码中是否有多条语句操作共享数据
    如何解决呢?(非常重要)
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中
    注意点:
    范围太小:没锁住所有有安全问题的代码 范围太大:没发挥多线程的功能。
  • 面试题synchronized 与 Lock的异同?
  • 相同:二者都可以解决线程安全问题
  • 不同:
    synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
    Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())
  • 使用的优先顺序:
    Lock—> 同步代码块(已经进入了方法体,分配了相应资源 ) —>同步方法(在方法体之外)
  • 利弊: 同步的方式,解决了线程的安全问题。—好处
    操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
  1. Java是如何解决线程安全问题的,有几种方式?并对比几种方式的不同
    利用同步锁的方式,有三种方式同步代码块、同步方法和用lock方法
  • 线程安全的单例模式

使用同步机制将单例模式中的懒汉式改写为线程安全的。

class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}
  • 死锁问题

死锁的理解: 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明:
出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续

我们使用同步时,要避免出现死锁。
死锁举例:

public static void main(String[] args) {

    StringBuffer s1 = new StringBuffer();
    StringBuffer s2 = new StringBuffer();


    new Thread(){
        @Override
        public void run() {
            synchronized (s1){
                s1.append("a");
                s2.append("1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (s2){
                    s1.append("b");
                    s2.append("2");
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }
    }.start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (s2){
                s1.append("c");
                s2.append("3");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (s1){
                    s1.append("d");
                    s2.append("4");
                    System.out.println(s1);
                    System.out.println(s2);
                }
            }
        }
    }).start();
}

线程通讯

为了解决线程的死锁问题,引入线程通讯

  • 线程通信涉及到的三个方法:

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明:

wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

代码示例:

// 使用两个线程打印 1-100,线程1, 线程2 交替打印。
class MyThread implements Runnable {
    private int number = 1;
    private Object object = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (object) {
                object.notify();//调用notify()方法唤醒线程
                if (number <= 100) {
                    //线程休眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + number);
                    number++;
                    try {
                        object.wait();//打印输出一次后调用wait()方法将线程阻塞
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();

        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);

        thread1.setName("线程1:");
        thread2.setName("线程2:");

        thread1.start();
        thread2.start();
    }
}
  • 面试sleep() 和 wait()的异同?
  • 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
  • 不同点:
    • 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
    • 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
    • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
  • 释放锁的操作:
  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到 break、 return终止了该代码块该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或 Exception,导致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的 wait()方法,当前线程暂停,并释放锁
  • 不会释放锁的操作:

线程执行同步代码块或同步方法时,程序调用 Thread. sleep()、Thread yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的 suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
应尽量避免使用 suspend()和 resume()来控制线程

实现Callable接口创建线程

实现方法:

创建一个实现Callable的实现类
实现call方法,将此线程需要执行的操作声明在call()中
创建Callable接口实现类的对象
将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

获取Callable中call方法的返回值

代码示例:

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

call()可以返回值的。
call()可以抛出异常,被外面的操作捕获,获取异常的信息
Callable是支持泛型的

  1. 新增方式二:使用线程池
    背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程对性能影响很大。
    解决方案:
    提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
    实现方法:

提供指定线程数量的线程池
执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
关闭连接池

相关API:
JDK 5.0起提供了线程池相关AP|: Executor Service和 Executors

Executor Service:真正的线程池接口。常见子类 Thread Poolexecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callabletask):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors. newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFⅸedthreadPool(n);创建一个可重用固定线程数的线程池
EXecutors. newSingleThreadEXecutor():创建一个只有一个线程的线程池
Executors. new thread Poo(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

复制代码
代码示例:
class NumberThread implements Runnable{

@Override
public void run() {
    for(int i = 0;i <= 100;i++){
        if(i % 2 == 0){
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

}

class NumberThread1 implements Runnable{

@Override
public void run() {
    for(int i = 0;i <= 100;i++){
        if(i % 2 != 0){
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

}

public class ThreadPool {

public static void main(String[] args) {
    //1. 提供指定线程数量的线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
    //设置线程池的属性

// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();

    //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
    service.execute(new NumberThread());//适合适用于Runnable
    service.execute(new NumberThread1());//适合适用于Runnable

// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}

}

复制代码
应用线程池的好处:

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中线程,不需要每次都创建)

便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止

面试题:Java中多线程的创建有几种方式?四种。
JDK 5.0以前:

即继承Thread类重run方法
实现Runnable接口实现run方法

JDK 5.0以后:

实现callable接口,实现call方法
利用线程池

线程的创建和使用

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性
	每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主称为线程体
	通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

API中创建线程的两种方式

JDK1.5之前创建新执行线程有两种方法:
继承Thread类的方式
实现Runnable接口的方式

方式一:继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();
        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        t1.start();
        
        //问题一:我们不能通过直接调用run()的方式启动线程。
		// t1.run();

        //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
		//t1.start();
        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();
        
        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }

}

注意点:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

方式二:实现Runnable接口

  1. 创建一个实现了Runnable接口的类
  2. 实现Runnable中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
          	  System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }
}

继承方式和实现方式的联系与区别

public class Thread extends Object implements Runnable

区别
	继承Thread:线程代码存放Thread子类run方法中。
	实现Runnable:线程代码存在接口的子类的run方法。
	用到的设计原则:组合替代继承
实现方式的好处
	避免了单继承的局限性
	多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

新增方式一:实现Callable接口

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

call()可以有返回值的。
call()可以抛出异常,被外面的操作捕获,获取异常的信息
Callable是支持泛型的

Future接口

可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutrueTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

新增方式二:使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止 …

线程池相关API

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown() :关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable
//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

Thread类

构造器
	Thread():创建新的Thread对象
	Thread(String threadname):创建线程并指定线程实例名
	Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
	Thread(Runnable target, String name):创建新的Thread对象

Thread类的常用方法

void start():启动当前线程;调用当前线程的run()
void run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
static Thread currentThread():静态方法,返回执行当前代码的线程
String getName():获取当前线程的名字
void setName(String name):设置当前线程的名字
static void yield():释放当前cpu的执行权
void join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
void stop():已过时。当执行此方法时,强制结束当前线程。
static void sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
boolean isAlive():判断当前线程是否存活

Java的调度方法

同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略

线程的优先级

线程的优先级等级
	MAX_PRIORITY:10
	MIN _PRIORITY:1
	NORM_PRIORITY:5
涉及的方法
	int getPriority() :返回线程优先值
	void setPriority(int newPriority) :改变线程的优先级
说明
	线程创建时继承父线程的优先级
	低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用
thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
Java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出。
形象理解:兔死狗烹,鸟尽弓藏

线程的生命周期

JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
	新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
	就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
	运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
	阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
	死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束线程的生命周期

线程的同步

问题的原因:
	当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:
	对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以

Synchronized

方式一:同步代码块

synchronized(同步监视器){
	//需要被同步的代码
}
说明:1.操作共享数据的代码,即为需要被同步的代码。  -->不能包含代码多了,也不能包含代码少了。
	 2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
	 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
				 要求:多个线程必须要共用同一把锁。
/**
* 实现Runnuble接口类 中的同步方式
*/
class Window1 implements Runnable{
    private int ticket = 100;
    
    @Override
    public void run() {
        while(true){
            synchronized (this){//此时的this:唯一的Window1的对象   //方式二:synchronized (dog) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

/**
 * 使用同步代码块解决继承Thread类的方式的线程安全问题
 */
class Window2 extends Thread{
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

方式二:同步方法

synchronized放在方法声明中,表示整个方法为同步方法。
public synchronized void show (String name){
	...
}
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
/**
 * 使用同步方法解决实现Runnable接口的线程安全问题
 */
 class Window3 implements Runnable {
    private int ticket = 100;

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

    private synchronized void show(){//同步监视器:this
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }
        }
    }
}

/**
 * 使用同步方法处理继承Thread类的方式中的线程安全问题
 */
 class Window4 extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){//同步监视器:Window4.class
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

Java对于多线程的安全问题提供了专业的解决方式:同步机制

Lock

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码;
        }
        finally{
            lock.unlock();
        }
    }
}

注意:如果同步代码有异常,要将unlock()写入finally语句块

Synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
    隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
    更好的扩展性(提供更多的子类)
    优先使用顺序:
    →Lock
    →同步代码块(已经进入了方法体,分配了相应资源)
    →同步方法(在方法体之外)

同步机制中的锁

同步锁机制: 
	在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 
	防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。
	第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
	任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
	同步方法的锁:静态方法(类名.class)、非静态方法(this)
	同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
	必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
	一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

同步的范围

1、如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
3、切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。

释放锁的操作

当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、
Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程

单例设计模式之懒汉式(线程安全)

class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

线程的死锁问题

死锁
	不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
	出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
	专门的算法、原则
	尽量减少同步资源的定义
	尽量避免嵌套同步
public class DeadLockTest {
    public static void main(String[] args) {
        final StringBuffer s1 = new StringBuffer();
        final StringBuffer s2 = new StringBuffer();
        new Thread() {
            public void run() {
                synchronized (s1) {
                    s2.append("A");
                    synchronized (s2) {
                        s2.append("B");
                        System.out.print(s1);
                        System.out.print(s2);
                    }
                }
            }
        }.start();
        new Thread() {
            public void run() {
                synchronized (s2) {
                    s2.append("C");
                    synchronized (s1) {
                        s1.append("D");
                        System.out.print(s2);
                        System.out.print(s1);
                    }
                }
            }
        }.start();
    }
}

线程的通信

//多线程会交替调用该方法
class Communication implements Runnable {
    int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() +
                            ":" + i++);
                } else
                    break;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

wait() 与 notify() 和 notifyAll()

wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

wait() 方法

在当前线程中调用方法: 对象名.wait()
使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出notify(或notifyAll) 为止。
调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
调用此方法后,当前线程将释放对象监控权 ,然后进入等待
在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()

在当前线程中调用方法: 对象名.notify()
功能:唤醒等待该对象监控权的一个/所有线程。
调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

sleep() 和 wait()的异同

1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
	1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
	2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
	3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

经典例题:生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
	生产者比消费者快时,消费者会漏掉一些数据没有取到。
	消费者比生产者快时,消费者会取相同的数据。
class Clerk { // 售货员
    private int product = 0;
    public synchronized void addProduct() {
        if (product >= 20) {
            try {
                wait();
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("生产者生产了
                    第" + product + "个产品");
                    notifyAll();
        }
    }
    public synchronized void getProduct() {
        if (this.product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消费者取走了第" +
                    product + "个产品");
            product--;
            notifyAll();
        }
    }
}

class Productor implements Runnable { // 生产者
    Clerk clerk;
    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    public void run() {
        System.out.println("生产者开始生产产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

class Consumer implements Runnable { // 消费者
    Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    public void run() {
        System.out.println("消费者开始取走产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.getProduct();
        }
    }
}

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Thread productorThread = new Thread(new Productor(clerk));
        Thread consumerThread = new Thread(new Consumer(clerk));
        productorThread.start();
        consumerThread.start();
    }
}

模拟银行取钱的问题

1.定义一个Account类
	1)该Account类封装了账户编号(String)和余额(double)两个属性
	2)设置相应属性的getter和setter方法
	3)提供无参和有两个参数的构造器
	4)系统根据账号判断与用户是否匹配,需提供hashCode()和equals()方法的重写
2.提供两个取钱的线程类:小明、小明’s wife
	1)提供了Account类的account属性和double类的取款额的属性
	2)提供带线程名的构造器
	3)run()方法中提供取钱的操作
3.在主类中创建线程进行测试。考虑线程安全问题。
class Account {
    private String accountId;
    private double balance;
    public Account() {
    }
    public Account(String accountId, double balance)
    {
        this.accountId = accountId;
        this.balance = balance;
    }
    public String getAccountId() {
        return accountId;
    }
    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }
    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
    public String toString() {
        return "Account [accountId=" + accountId + ", balance=" + balance + "]";
    }
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((accountId == null) ? 0 :
                accountId.hashCode());
        long temp;
        temp = Double.doubleToLongBits(balance);
        result = prime * result + (int) (temp ^ (temp >>>
                32));
        return result;
    }
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Account other = (Account) obj;
        if (accountId == null) {
            if (other.accountId != null)
                return false;
        } else if (!accountId.equals(other.accountId))
            return false;
        if (Double.doubleToLongBits(balance) !=
                Double.doubleToLongBits(other.balance))
            return false;
        return true;
    }
}

class WithDrawThread extends Thread {
    Account account;
    // 要取款的额度
    double withDraw;
    public WithDrawThread(String name, Account account, double amt) {
        super(name);
        this.account = account;
        this.withDraw = amt;
    }
    public void run() {
        synchronized (account) {
            if (account.getBalance() > withDraw) {
                System.out.println(Thread.currentThread().getName() + ":取款成功,取现的金额为:" + withDraw);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance() - withDraw);
            } else {
                System.out.println("取现额度超过账户余额,取款失败");
            }
            System.out.println("现在账户的余额为:" + account.getBalance());
        }
    }
}
public class WithDrawThreadTest {
    public static void main(String[] args) {
        Account account = new Account("1234567", 10000);
        Thread t1 = new WithDrawThread("小明", account, 8000);
        Thread t2 = new WithDrawThread("小明's wife", account, 2800);
        t1.start();
        t2.start();
    }
}

Java 8 新特性

  • 增加了新的语法:Lambda表达式
  • 引入强大的 Stream APl
  • 最大化减少空指针异常:Optional
  • Nashorn 引擎,允许在JVM上运行 JS 应用
  • 并行流
    并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
    Java 8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换

Lambda 表达式

什么是Lamada表达式

基本含义:

Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

Java中:

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

为什么引入Lamada表达式

引入Lamada表达式前,在Java中传递一个代码段并不容易,不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象, 这个对象的类需要有一个方法能包含所需的代码。

Lamada语法

1.参数,箭头(→)以及一个表达式。

(String first, String second)-> 
	first.length()-second.length()
(String first, String second)-> {
	if (first.length()<second.length())
		return -1;
	else if (first.length()>second.length())
		return 1;
	else return 0;
}
()-> { 
	for(int i=100; i>=0; i--) 
		System.out.printIn(i); 
}
Comparator<String> comp=(first,second) // Same as (String first, String second)
	-> first.1ength()-second.length();
ActionListener listener = event ->
	System.out.print1n("The time is " + new Date());
(int x)->{
	if(x>= 0)
		return 1;
}

Lamada应用实例

  • Lamdba 表达式概述

Lambda 是一个匿名函数,可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升。

  • 使用 Lambda 表达式前后对比

示例一:调用 Runable 接口

@Test
public void test1(){
    //未使用Lambda表达式的写法
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            System.out.println("hello Lambda!");
        }
    };
    r1.run();

    System.out.println("========================");
    //Lamdba表达式写法
    Runnable r2 = () -> System.out.println("hi Lambda!");
    r2.run();
}

示例二:使用Comparator接口

@Test
public void test2(){
    //未使用Lambda表达式的写法
    Comparator<Integer> com1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return Integer.compare(o1,o2);
        }
    };

    int compare1 = com1.compare(12, 32);
    System.out.println(compare1);//-1
    System.out.println("===================");

    //Lambda表达式的写法
    Comparator<Integer> com2 = (o1,o2) -> Integer.compare(o1,o2);

    int compare2 = com2.compare(54, 21);
    System.out.println(compare2);//1
    System.out.println("===================");

    //方法引用
    Comparator<Integer> cpm3 = Integer::compareTo;
    int compare3 = cpm3.compare(12, 12);
    System.out.println(compare3);//0
}
  • Lamdba表达式基本语法
    (o1,o2) -> Integer.compare(o1,o2);

-> :lambda 操作符 或 箭头操作符
-> 左边:lambda 形参列表 (其实就是接口中的抽象方法的形参列表)
-> 右边:lambda 体(其实就是重写的抽象方法的方法体)

  • Lamdba表达式使用(包含六种情况)
    • 无参,有返回值
      Runnable r1 = () -> {System.out.println(“hello Lamdba!”)}
    • Lamdba需要一个参数,但没有返回值
      Consumer<String> con = (String str) -> {System.out.println(str)}
    • 数据类型可省略,因为可由编译器推断得出,称为类型推断
      Consumer<String> con = (str) -> {System.out.println(str)}
    • Lamdba若只需要一个参数时,小括号可以省略
      Consumer<String> con = str -> {System.out.println(str)}
    • Lamdba需要两个以上的参数,多条执行语句,并且可以有返回值
      Comparator<Integer>com = (o1,o1) -> {
      	Syste.out.println("Lamdba表达式使用");
          return Integer.compare(o1,o2);
      }
      
    • 当Lamdba体只有一条语句时,return和大括号若有,都可以省略
      Comparator<Integer>com = (o1,o1) -> Integer.compare(o1,o2);

代码示例:

public class LamdbaTest2 {
    //语法格式一:无参,无返回值
    @Test
    public void test1() {
        //未使用Lambda表达式
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello Lamdba");
            }
        };
        r1.run();
        System.out.println("====================");
        //使用Lambda表达式
        Runnable r2 = () -> {
            System.out.println("Hi Lamdba");
        };
        r2.run();
    }

    //语法格式二:Lambda 需要一个参数,但是没有返回值。
    @Test
    public void test2() {
        //未使用Lambda表达式
        Consumer<String> con = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好啊Lambda!");
        System.out.println("====================");
        //使用Lambda表达式
        Consumer<String> con1 = (String s) -> {
            System.out.println(s);
        };
        con1.accept("我是Lambda");

    }

    //语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
    @Test
    public void test3() {
        //未使用Lambda表达式
        Consumer<String> con = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好啊Lambda!");
        System.out.println("====================");
        //使用Lambda表达式
        Consumer<String> con1 = (s) -> {
            System.out.println(s);
        };
        con1.accept("我是Lambda");
    }

    @Test
    public void test(){
        ArrayList<String> list = new ArrayList<>();//类型推断,用左边推断右边
        int[] arr = {1,2,3,4};//类型推断,用左边推断右边
    }

    //语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
    @Test
    public void test4() {
        //未使用Lambda表达式
        Consumer<String> con = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con.accept("你好啊Lambda!");
        System.out.println("====================");
        //使用Lambda表达式
        Consumer<String> con1 = s -> {
            System.out.println(s);
        };
        con1.accept("我是Lambda");
    }

    //语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
    @Test
    public void test5() {
        //未使用Lambda表达式
        Comparator<Integer> com1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                System.out.println(o1);
                System.out.println(o2);
                return Integer.compare(o1, o2);
            }
        };
        System.out.println(com1.compare(23, 45));
        System.out.println("====================");
        //使用Lambda表达式
        Comparator<Integer> com2 = (o1, o2) -> {
            System.out.println(o1);
            System.out.println(o2);
            return o1.compareTo(o2);
        };
        System.out.println(com2.compare(23, 12));
    }

    //语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
    @Test
    public void test6() {
        //未使用Lambda表达式
        Comparator<Integer> com1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return Integer.compare(o1, o2);
            }
        };
        System.out.println(com1.compare(23, 45));
        System.out.println("====================");
        //使用Lambda表达式
        Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2);

        System.out.println(com2.compare(23, 12));
    }
    @Test
    public void test7(){
        //未使用Lambda表达式
        Consumer<String> con1 = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con1.accept("hi!");
        System.out.println("====================");
        //使用Lambda表达式
        Consumer<String> con2 = s -> System.out.println(s);
        con2.accept("hello");
    }

}
  • Lambda 表达式使用总结

-> 左边:lambda 形参列表的参数类型可以省略(类型推断);如果 lambda 形参列表只有一个参数,其一对 () 也可以省略
-> 右边:lambda 体应该使用一对 {} 包裹;如果 lambda 体只有一条执行语句(可能是 return 语句),省略这一对 {} 和 return 关键字

  • Lamdba表达式总结

Lambda 表达式的本质:作为函数式接口的实例
如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。
因此以前用匿名实现类表示的现在都可以用 Lambda 表达式来写。

函数式接口

只包含一个抽象方法的接口,称为函数式接口。
可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
Lambda 表达式的本质:作为函数式接口的实例

  • 自定义函数式接口
@FunctionalInterface
public interface MyInterface {
    void method1();
}
  • 四大核心函数式接口

应用举例

public class LambdaTest3 {
    //    消费型接口 Consumer<T>     void accept(T t)
    @Test
    public void test1() {
        //未使用Lambda表达式
        Learn("java", new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println("学习什么? " + s);
            }
        });
        System.out.println("====================");
        //使用Lambda表达
        Learn("html", s -> System.out.println("学习什么? " + s));

    }

    private void Learn(String s, Consumer<String> stringConsumer) {
        stringConsumer.accept(s);
    }

    //    供给型接口 Supplier<T>     T get()
    @Test
    public void test2() {
        //未使用Lambdabiaodas
        Supplier<String> sp = new Supplier<String>() {
            @Override
            public String get() {
                return new String("我能提供东西");
            }
        };
        System.out.println(sp.get());
        System.out.println("====================");
        //使用Lambda表达
        Supplier<String> sp1 = () -> new String("我能通过lambda提供东西");
        System.out.println(sp1.get());
    }

    //函数型接口 Function<T,R>   R apply(T t)
    @Test
    public void test3() {
        //使用Lambda表达式
        Employee employee = new Employee(1001, "Tom", 45, 10000);

        Function<Employee, String> func1 =e->e.getName();
        System.out.println(func1.apply(employee));
        System.out.println("====================");

        //使用方法引用
        Function<Employee,String>func2 = Employee::getName;
        System.out.println(func2.apply(employee));

    }

    //断定型接口 Predicate<T>    boolean test(T t)
    @Test
    public void test4() {
        //使用匿名内部类
        Function<Double, Long> func = new Function<Double, Long>() {
            @Override
            public Long apply(Double aDouble) {
                return Math.round(aDouble);
            }
        };
        System.out.println(func.apply(10.5));
        System.out.println("====================");

        //使用Lambda表达式
        Function<Double, Long> func1 = d -> Math.round(d);
        System.out.println(func1.apply(12.3));
        System.out.println("====================");

        //使用方法引用
        Function<Double,Long>func2 = Math::round;
        System.out.println(func2.apply(12.6));

    }
}
  • 何时使用lambda表达式?

当需要对一个函数式接口实例化的时候,可以使用 lambda 表达式。

  • 何时使用给定的函数式接口?

如果我们开发中需要定义一个函数式接口,首先看看在已有的jdk提供的函数式接口是否提供了能满足需求的函数式接口。如果有,则直接调用即可,不需要自己再自定义了。

方法的引用

概述: 方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是 Lambda 表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法。
使用情景: 当要传递给 Lambda 体的操作,已经实现的方法了,可以使用方法引用!
使用格式: 类(或对象) :: 方法名

  • 使用方式
    对象 :: 非静态方法
    类 :: 静态方法
    类 :: 非静态方法
  • 使用要求

要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2)
当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName(针对于情况3)

  • 使用建议

如果给函数式接口提供实例,恰好满足方法引用的使用情境,就可以考虑使用方法引用给函数式接口提供实例。如果不熟悉方法引用,那么还可以使用 lambda 表达式。

  • 使用举例
public class MethodRefTest {

    // 情况一:对象 :: 实例方法
    //Consumer中的void accept(T t)
    //PrintStream中的void println(T t)
    @Test
    public void test1() {
        //使用Lambda表达
        Consumer<String> con1 = str -> System.out.println(str);
        con1.accept("中国");
        System.out.println("====================");

        //使用方法引用
        PrintStream ps = System.out;
        Consumer con2 = ps::println;
        con2.accept("China");
    }

    //Supplier中的T get()
    //Employee中的String getName()
    @Test
    public void test2() {
        //使用Lambda表达
        Employee emp = new Employee(1001, "Bruce", 34, 600);
        Supplier<String> sup1 = () -> emp.getName();
        System.out.println(sup1.get());
        System.out.println("====================");

        //使用方法引用
        Supplier sup2 = emp::getName;
        System.out.println(sup2.get());
    }

    // 情况二:类 :: 静态方法
    //Comparator中的int compare(T t1,T t2)
    //Integer中的int compare(T t1,T t2)
    @Test
    public void test3() {
        //使用Lambda表达
        Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
        System.out.println(com1.compare(32, 45));
        System.out.println("====================");

        //使用方法引用
        Comparator<Integer> com2 = Integer::compareTo;
        System.out.println(com2.compare(43, 34));
    }

    //Function中的R apply(T t)
    //Math中的Long round(Double d)
    @Test
    public void test4() {
        //使用匿名内部类
        Function<Double, Long> func = new Function<Double, Long>() {
            @Override
            public Long apply(Double aDouble) {
                return Math.round(aDouble);
            }
        };
        System.out.println(func.apply(10.5));
        System.out.println("====================");

        //使用Lambda表达式
        Function<Double, Long> func1 = d -> Math.round(d);
        System.out.println(func1.apply(12.3));
        System.out.println("====================");

        //使用方法引用
        Function<Double, Long> func2 = Math::round;
        System.out.println(func2.apply(12.6));
    }

    // 情况三:类 :: 实例方法
    // Comparator中的int comapre(T t1,T t2)
    // String中的int t1.compareTo(t2)
    @Test
    public void test5() {
        //使用Lambda表达式
        Comparator<String> com1 = (s1, s2) -> s1.compareTo(s2);
        System.out.println(com1.compare("abd", "aba"));
        System.out.println("====================");

        //使用方法引用
        Comparator<String> com2 = String::compareTo;
        System.out.println(com2.compare("abd", "abc"));
    }

    //BiPredicate中的boolean test(T t1, T t2);
    //String中的boolean t1.equals(t2)
    @Test
    public void test6() {
        //使用Lambda表达式
        BiPredicate<String, String> pre1 = (s1, s2) -> s1.equals(s2);
        System.out.println(pre1.test("abc", "abc"));
        System.out.println("====================");

        //使用方法引用
        BiPredicate<String, String> pre2 = String::equals;
        System.out.println(pre2.test("abc", "abd"));

    }

    // Function中的R apply(T t)
    // Employee中的String getName();
    @Test
    public void test7() {
        //使用Lambda表达式
        Employee employee = new Employee(1001, "Tom", 45, 10000);

        Function<Employee, String> func1 =e->e.getName();
        System.out.println(func1.apply(employee));
        System.out.println("====================");

        //使用方法引用
        Function<Employee,String>func2 = Employee::getName;
        System.out.println(func2.apply(employee));
    }
}

构造器引用

和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。抽象方法的返回值类型即为构造器所属的类的类型
类名::new

  • 案例
//构造器引用
//Supplier中的T get()
@Test
public void test1() {
    //使用匿名内部类
    Supplier<Employee> sup = new Supplier<Employee>() {
        @Override
        public Employee get() {
            return new Employee();
        }
    };
    System.out.println(sup.get());
    //使用Lambda表达式
    System.out.println("====================");
    Supplier<Employee> sup1 = () -> new Employee(1001, "Tom", 43, 13333);
    System.out.println(sup1.get());

    //使用方法引用
    Supplier<Employee> sup2 = Employee::new;
    System.out.println(sup2.get());

}

//Function中的R apply(T t)
@Test
public void test2() {
    //使用Lambda表达式
    Function<Integer, Employee> func1 = id -> new Employee(id);
    Employee employee = func1.apply(1001);
    System.out.println(employee);
    System.out.println("====================");

    //使用方法引用
    Function<Integer, Employee> func2 = Employee::new;
    Employee employee1 = func2.apply(1002);
    System.out.println(employee1);

}

//BiFunction中的R apply(T t,U u)
@Test
public void test3() {
    //使用Lambda表达式
    BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);
    System.out.println(func1.apply(1001, "Tom"));
    System.out.println("====================");

    //使用方法引用
    BiFunction<Integer, String, Employee> func2 = Employee::new;
    System.out.println(func2.apply(1002, "Jarry"));
}

数组引用

可以把数组看做是一个特殊的类,则写法与构造器引用一致。
数组类型 [] :: new

  • 案例
//Function中的R apply(T t)
@Test
public void test4() {
    Function<Integer, String[]> func1 = length -> new String[length];
    String[] arr1 = func1.apply(5);
    System.out.println(Arrays.toString(arr1));

    System.out.println("====================");

    //使用方法引用
    Function<Integer,String[]>func2=String[]::new;
    String[] arr2 = func2.apply(10);
    System.out.println(Arrays.toString(arr2));
}

StreamAPI

  1. Stream API概述

Stream 关注的是对数据的运算,与 CPU 打交道;集合关注的是数据的存储,与内存打交道;
Java 8 提供了一套 api ,使用这套 api 可以对内存中的数据进行过滤、排序、映射、归约等操作。类似于 sql 对数据库中表的相关操作。
Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据, Stream讲的是计算!”

使用注意点:
① Stream 自己不会存储元素。
② Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
③ Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
2. Stream 使用流程
① Stream 的实例化
② 一系列的中间操作(过滤、映射、…)
③ 终止操作

使用流程中的注意点:

一个中间操作链,对数据源的数据进行处理
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

创建 Stream

  • 通过集合
    Java 8的 Collection 接口被扩展,提供了两个获取流的方法:
    default Stream<E> stream() : 返回一个顺序流
    default Stream<E> parallelStream() : 返回一个并行流

  • 通过数组
    Java 8中的 Arrays 的静态方法 stream() 可以获取数组流
    调用 Arrays 类的 static<T> Stream<T> stream(T[] array): 返回一个流
    重载形式,能够处理对应基本类型的数组:
    public static IntStream stream(int[] array)
    public static LongStream stream(long[] array)
    public static DoubleStream stream(double[] array)

  • 通过Stream的of()方法
    可以调用Stream类静态方法of(),通过显示值创建一个流。可以用于接收任意数量的参数
    public static <T>Stream<T> of(T…values):返回一个流

  • 创建无限流
    迭代: public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    生成: public static<T> Stream<T> generate(Supplier<T> s)

代码示例:

public class StreamAPITest1 {
    //创建 Stream方式一:通过集合
    @Test
    public void test1() {
        List<Employee> employees = EmployeeData.getEmployees();
        //efault Stream<E> stream() : 返回一个顺序流
        Stream<Employee> stream = employees.stream();

        //default Stream<E> parallelStream() : 返回一个并行流
        Stream<Employee> employeeStream = employees.parallelStream();
    }

    //创建 Stream方式二:通过数组
    @Test
    public void test2() {
        int[] arrs = {1, 2, 3, 6, 2};
        //调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
        IntStream stream = Arrays.stream(arrs);

        Employee e1 = new Employee(1001, "Tom");
        Employee e2 = new Employee(1002, "Jerry");
        Employee[] employees = {e1, e2};
        Stream<Employee> stream1 = Arrays.stream(employees);
    }

    //创建 Stream方式三:通过Stream的of()
    @Test
    public void test3() {
        Stream<Integer> integerStream = Stream.of(12, 34, 45, 65, 76);
    }

    //创建 Stream方式四:创建无限流
    @Test
    public void test4() {

        //迭代
        //public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
        //遍历前10个偶数
        Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

        //生成
        //public static<T> Stream<T> generate(Supplier<T> s)
        Stream.generate(Math::random).limit(10).forEach(System.out::println);
    }
}

中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为惰性求值。

  • 筛选与切片

代码示例:

//1-筛选与切片,注意执行终止操作后,Stream流就被关闭了,使用时需要再次创建Stream流
@Test
public void test1(){
    List<Employee> employees = EmployeeData.getEmployees();
    //filter(Predicate p)——接收 Lambda , 从流中排除某些元素。
    Stream<Employee> employeeStream = employees.stream();
    //练习:查询员工表中薪资大于7000的员工信息
    employeeStream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);

    //limit(n)——截断流,使其元素不超过给定数量。
    employeeStream.limit(3).forEach(System.out::println);
    System.out.println();

    //skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
    employeeStream.skip(3).forEach(System.out::println);
    //distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
    employees.add(new Employee(1010,"刘庆东",56,8000));
    employees.add(new Employee(1010,"刘庆东",56,8000));
    employees.add(new Employee(1010,"刘庆东",56,8000));
    employees.add(new Employee(1010,"刘庆东",56,8000));

    employeeStream.distinct().forEach(System.out::println);
}
  • 映射

代码示例:

//2-映射
@Test
public void test2(){
    List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
    //map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
    list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

    //练习1:获取员工姓名长度大于3的员工的姓名。
    List<Employee> employees = EmployeeData.getEmployees();
    Stream<String> nameStream = employees.stream().map(Employee::getName);
    nameStream.filter(name -> name.length() >3).forEach(System.out::println);
    System.out.println();
    //练习2:使用map()中间操作实现flatMap()中间操作方法
    Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest2::fromStringToStream);
    streamStream.forEach(s ->{
        s.forEach(System.out::println);
    });
    System.out.println();
    //flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
    Stream<Character> characterStream = list.stream().flatMap(StreamAPITest2::fromStringToStream);
    characterStream.forEach(System.out::println);

}
//将字符串中的多个字符构成的集合转换为对应的Stream的实例
public static Stream<Character>fromStringToStream(String str){
    ArrayList<Character> list = new ArrayList<>();
    for (Character c :
         str.toCharArray()) {
        list.add(c);
    }
    return list.stream();
}
//map()和flatMap()方法类似于List中的add()和addAll()方法
@Test
public void test(){
    ArrayList<Object> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);
    list1.add(3);
    list1.add(4);

    ArrayList<Object> list2 = new ArrayList<>();
    list2.add(5);
    list2.add(6);
    list2.add(7);
    list2.add(8);

    list1.add(list2);
    System.out.println(list1);//[1, 2, 3, 4, [5, 6, 7, 8]]
    list1.addAll(list2);
    System.out.println(list1);//[1, 2, 3, 4, [5, 6, 7, 8], 5, 6, 7, 8]

}
  • 排序

代码示例:

//3-排序
@Test
public void test3(){
    //sorted()——自然排序
    List<Integer> list = Arrays.asList(12, 34, 54, 65, 32);
    list.stream().sorted().forEach(System.out::println);

    //抛异常,原因:Employee没有实现Comparable接口
    List<Employee> employees = EmployeeData.getEmployees();
    employees.stream().sorted().forEach(System.out::println);

    //sorted(Comparator com)——定制排序
    List<Employee> employees1 = EmployeeData.getEmployees();
    employees1.stream().sorted((e1,e2)->{
        int ageValue = Integer.compare(e1.getAge(), e2.getAge());
        if (ageValue != 0){
            return ageValue;
        }else {
            return -Double.compare(e1.getSalary(),e2.getSalary());
        }

    }).forEach(System.out::println);
}

终止操作

终止操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、 Integer,甚至是 void
流进行了终止操作后,不能再次使用。

  • 匹配与查找
//1-匹配与查找
@Test
public void test1(){
    List<Employee> employees = EmployeeData.getEmployees();

    //allMatch(Predicate p)——检查是否匹配所有元素。
    //练习:是否所有的员工的年龄都大于18
    boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
    System.out.println(allMatch);
    //anyMatch(Predicate p)——检查是否至少匹配一个元素。
    //练习:是否存在员工的工资大于 5000
    boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 5000);
    System.out.println(anyMatch);

    //noneMatch(Predicate p)——检查是否没有匹配的元素。
    //练习:是否存在员工姓“雷”
    boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷"));
    System.out.println(noneMatch);

    //findFirst——返回第一个元素
    Optional<Employee> first = employees.stream().findFirst();
    System.out.println(first);

    //findAny——返回当前流中的任意元素
    Optional<Employee> employee = employees.parallelStream().findAny();
    System.out.println(employee);
}

@Test
public void test2(){
    List<Employee> employees = EmployeeData.getEmployees();
    // count——返回流中元素的总个数
    long count = employees.stream().filter(e -> e.getSalary()>5000).count();
    System.out.println(count);

    //max(Comparator c)——返回流中最大值
    //练习:返回最高的工资
    Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
    Optional<Double> maxSalary = salaryStream.max(Double::compareTo);
    System.out.println(maxSalary);

    //min(Comparator c)——返回流中最小值
    //练习:返回最低工资的员工
    Optional<Double> minSalary = employees.stream().map(e -> e.getSalary()).min(Double::compareTo);
    System.out.println(minSalary);

    //forEach(Consumer c)——内部迭代
    employees.stream().forEach(System.out::println);
    System.out.println();
    //使用集合的遍历操作
    employees.forEach(System.out::println);
}
  • 归约

map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名

代码示例:

//2-归约
@Test
public void test3(){
    //reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
    //练习1:计算1-10的自然数的和
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Integer sum = list.stream().reduce(0, Integer::sum);
    System.out.println(sum);

    //reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
    //练习2:计算公司所有员工工资的总和
    List<Employee> employees = EmployeeData.getEmployees();
    Optional<Double> sumSalary = employees.stream().map(e -> e.getSalary()).reduce(Double::sum);
    System.out.println(sumSalary);
}
  • 收集

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)
Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例具体方法与实例如下表:

代码示例:

//3-收集
@Test
public void test4(){
    //collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
    //练习1:查找工资大于6000的员工,结果返回为一个List或Set
    List<Employee> employees = EmployeeData.getEmployees();
    List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());

    employeeList.forEach(System.out::println);
    System.out.println();
    Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());
    employeeSet.forEach(System.out::println);
}

Optional 类

为了解决 java 中的空指针问题
Optional 类(java.util.Optional) 是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

  • 创建 Optional 类对象的方法

Optional.of(T t) : 创建一个 Optional 实例,t 必须非空;
Optional.empty() : 创建一个空的 Optional 实例
Optional.ofNullable(T t):t 可以为 null

  • 判断Optional容器是否包含对象

boolean isPresent():判断是否包含对象
void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传给它。

  • 获取 Optional 容器的对象

T get():如果调用对象包含值,返回该值,否则抛异常
T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象
T orElseGet(Supplier<? extends t> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象。
T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常。

  • 搭配使用

of()和get() 方法搭配使用,明确对象非空
ofNullable()和orElse() 搭配使用,不确定对象非空

public class OptionalTest {
    @Test
    public void test1() {
        //empty():创建的Optional对象内部的value = null
        Optional<Object> op1 = Optional.empty();
        if (!op1.isPresent()){//Optional封装的数据是否包含数据
            System.out.println("数据为空");
        }
        System.out.println(op1);
        System.out.println(op1.isPresent());

        //如果Optional封装的数据value为空,则get()报错。否则,value不为空时,返回value.
        System.out.println(op1.get());
    }
    @Test
    public void test2(){
        String str = "hello";
//        str = null;
        //of(T t):封装数据t生成Optional对象。要求t非空,否则报错。
        Optional<String> op1 = Optional.of(str);
        //get()通常与of()方法搭配使用。用于获取内部的封装的数据value
        String str1 = op1.get();
        System.out.println(str1);
    }
    @Test
    public void test3(){
        String str ="Beijing";
        str = null;
        //ofNullable(T t) :封装数据t赋给Optional内部的value。不要求t非空
        Optional<String> op1 = Optional.ofNullable(str);
        System.out.println(op1);
        //orElse(T t1):如果Optional内部的value非空,则返回此value值。如果
        //value为空,则返回t1.
        String str2 = op1.orElse("shanghai");
        System.out.println(str2);
    }
}
  • 使用 Optional 类避免产生空指针异常
public class GirlBoyOptionalTest {

    //使用原始方法进行非空检验
    public String getGrilName1(Boy boy){
        if (boy != null){
            Girl girl = boy.getGirl();
            if (girl != null){
                return girl.getName();
            }
        }
        return null;
    }
    //使用Optional类的getGirlName()进行非空检验
    public String getGirlName2(Boy boy){
        Optional<Boy> boyOptional = Optional.ofNullable(boy);
        //此时的boy1一定非空,boy为空是返回“迪丽热巴”
        Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));

        Girl girl = boy1.getGirl();
        //girl1一定非空,girl为空时返回“古力娜扎”
        Optional<Girl> girlOptional = Optional.ofNullable(girl);
        Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));

        return girl1.getName();
    }

    //测试手动写的控制检测
    @Test
    public void test1(){

        Boy boy = null;
        System.out.println(getGrilName1(boy));

        boy = new Boy();
        System.out.println(getGrilName1(boy));

        boy = new Boy(new Girl("杨幂"));
        System.out.println(getGrilName1(boy));
    }
    //测试用Optional类写的控制检测
    @Test
    public void test2(){
        Boy boy = null;
        System.out.println(getGirlName2(boy));

        boy = new Boy();
        System.out.println(getGirlName2(boy));

        boy = new Boy(new Girl("杨幂"));
        System.out.println(getGirlName2(boy));

    }
}

反射的支持增强

提高了创建对象、对象赋值和反射创建对象的时间
代码示例:

public class testReflection {
    // 循环次数10亿次
    private static final int loopCnt = 1000 * 1000 * 1000;

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        // 输出jdk版本
        System.out.println("java version is" + System.getProperty("java.version"));
        creatNewObject();
        optionObject();
        reflectCreatObject();
    }

    // person对象
    static class Person {
        private Integer age = 20;

        public Integer getAge() {
            return age;
        }

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

    // 每次创建新对象
    public static void creatNewObject() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < loopCnt; i++) {
            Person person = new Person();
            person.setAge(30);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("循环十亿次创建对象所需的时间:" + (endTime - startTime));
    }

    // 为同一个对象赋值
    public static void optionObject() {
        long startTime = System.currentTimeMillis();
        Person p = new Person();
        for (int i = 0; i < loopCnt; i++) {
            p.setAge(10);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("循环十亿次为同一对象赋值所需的时间:" + (endTime - startTime));
    }

    // 通过反射创建对象
    public static void reflectCreatObject() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        long startTime = System.currentTimeMillis();
        Class<Person> personClass = Person.class;
        Person person = personClass.newInstance();
        Method setAge = personClass.getMethod("setAge", Integer.class);
        for (int i = 0; i < loopCnt; i++) {
            setAge.invoke(person, 90);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("循环十亿次反射创建对象所需的时间:" + (endTime - startTime));
    }
}
  • 输出
  • 编译级别为JDK8时
    java version is 1.8.0_201
    循环十亿次创建对象所需的时间:9
    循环十亿次为同一对象赋值所需的时间:59
    循环十亿次反射创建对象所需的时间:2622
  • 编译级别为JDK7时
    java version is 1.7
    循环十亿次创建对象所需的时间:6737
    循环十亿次为同一对象赋值所需的时间:3394
    循环十亿次反射创建对象所需的时间:293603

面试

  • Java语言版本构成及特性

简单性、面向对象、分布式、健壮性、安全性、可移植性、解释型、高性能、多线程、动态性

问题解决

java:Compilation failed

在这里插入图片描述
确认:

1. Project Structure

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. Settings

在这里插入图片描述
在这里插入图片描述

3. pom.xml(刚开始就设置好这里就不会出现该问题)

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/28429.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Go:日志滚动(rolling)记录器 lumberjack 简介

文章目录简介简单使用1. Logger 结构体2. backup日志文件的文件名3. 获取文件句柄4. 日志文件backup5. 日志滚动后处理6. 收集旧日志文件7. 后处理小结简介 lumberjack是一个日志滚动记录器。写入lumberjack的日志达到一定的条件后会进行存档&#xff08;普通文件的形式&#…

TAT (AYGRKKRRQRRR)

TAT (AYGRKKRRQRRR) 是一种细胞穿膜肽, 能够将各种性质的药物高效率地传递进入细胞&#xff0c;该传递过程不需要配体-受体特异性结合, 且无饱和现象。但 TAT 缺乏细胞选择性, 能够穿透所有细胞膜, 这一缺点极大地限制了其在全身给药的肿瘤靶向系统中的应用。 编号: 402555中文…

电脑麦克风没声音怎么办?3个方法快速解决

当你跟朋友电脑语音聊天的时候&#xff0c;一连说了好几段话&#xff0c;结果朋友发消息告诉你&#xff0c;问你怎么一直不吭声&#xff0c;你这才发现&#xff0c;原来是你自己电脑麦克风没声音。电脑麦克风没声音怎么办&#xff1f;电脑麦克风说话别人听不到怎么回事&#xf…

机器学习笔记之核方法(一)核方法思想与核函数介绍

机器学习笔记之核方法——核方法思想与核函数介绍引言回顾&#xff1a;支持向量机的对偶问题核方法思想介绍线性可分与线性不可分非线性带来高维转换对偶表示带来内积核函数核函数的定义(2022/11/23)正定核函数引言 本节将介绍核方法以及核函数。 回顾&#xff1a;支持向量机…

[附源码]java毕业设计学生宿舍管理系统设计

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

[附源码]java毕业设计新生入学计算机配号系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

代码随想录63——额外题目【链表】:234回文链表、143重排链表、141环形链表

文章目录1.234回文链表1.1.题目1.2.解答1.2.1.数组模拟方法1.2.2.反转后半部分链表法2.143重排链表2.1.题目2.2.解答3.141环形链表3.1.题目3.2.解答1.234回文链表 参考&#xff1a;代码随想录&#xff0c;234回文链表&#xff1b;力扣题目链接 1.1.题目 1.2.解答 1.2.1.数组…

Qt-FFmpeg开发-视频播放(5)

Qt-FFmpeg开发-视频播放【软/硬解码 OpenGL显示YUV/NV12】 文章目录Qt-FFmpeg开发-视频播放【软/硬解码 OpenGL显示YUV/NV12】1、概述2、实现效果3、FFmpeg硬解码流程4、优化av_hwframe_transfer_data()性能低问题5、主要代码5.1 解码代码5.2 OpenGL显示RGB图像代码6、完整源…

Java面试题——进程和线程的关系

并发编程 很早以前的计算机上只能执行一个程序&#xff0c;在该程序执行时&#xff0c;下一个执行流只能等待该程序执行结束&#xff0c;我们认为这种依次执行的方式十分浪费资源且效率低下&#xff08;因为一个程序执行只会消耗计算机的部分资源&#xff0c;其他资源同一时刻…

对 Masa.Blazor.Maui.Plugin.GeTuiPushBinding 项目的引用

新建一个 MAUI Blazor 项目&#xff1a;Masa.Blazor.Maui.Plugin.GeTuiSample, 添加对 Masa.Blazor.Maui.Plugin.GeTuiPushBinding 项目的引用 1、初始化个推 SDK 个推 SDK 的初始化在 MainActivity.OnCreate () 或 MainApplication.OnCreate () 方法中都是可以的&#xff0c…

使用Docker+Jenkins+Gitee自动化部署SpringBoot项目

目录搭建基础环境1、使用Docker-Compose搭建基础环境2、搭建项目仓库环境&#xff0c;创建Dockerfile文件3、配置Jenkins3.1、初始化Jenkins3.2、安装核心插件3.3、全局工具配置3.3.1、配置Git。3.3.2、配置Maven3.3.3、配置JDK3.4、配置Git凭证3.5、构建项目3.5.1、配置源码管…

Docker教程(centos下安装及docker hello world)

Docker介绍 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows操作系统的机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c;相互之间不会有任何…

STC51单片机38——按键控制舵机连续运动,稳定不抖动

仿真&#xff1a; //开发板按钮K3和K4&#xff0c;舵机信号线P27 //程序为12m晶振&#xff0c;开发板为11.0592M #include"reg52.h" #define u8 unsigned char #define u16 unsigned int sbit P27P2^7;//舵机信号线 sbit K3P3^2; //正偏转 sbit K4P3^3; //反偏…

我的大二web课程设计 使用HTML做一个简单漂亮的页面(纯html代码)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Flutter中GetX系列四--BottomSheet(底部弹框)

BottomSheet介绍 BottomSheet 是底部弹出的一个组件&#xff0c;常用于单选、验证码二次校验弹窗等&#xff0c;GetX的BottomSheet底部弹出是自定义通过路由push的方法实现底部弹窗的一个效果。 BottomSheet使用 我们可以通过GetX很轻松的调用bottomSheet()&#xff0c;而且…

HTML+CSS大作业:使用html设计一个简单好看的公司官网首页 浮动布局

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

web前端期末大作业:网站设计与实现——咖啡网站HTML+CSS+JavaScript

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

PMBOK史上最大的改版,你知道到底有什么精华嘛?

1、项目管理经典的5大过程组、10大知识领域和49个过程&#xff08;第六版&#xff09;的结构彻底被改变&#xff0c;取而代之的是项目管理12大原则和8大绩效域组成的全新结构&#xff1b; 2、项目管理标准顺应趋势&#xff0c;从基于过程&#xff08;Process-based&#xff09…

AI+保险,打造让投保人“叫绝”的服务方式

近年来,信息技术在保险领域的应用越来越广泛&#xff0c;在稳步推进保险业务的线上化与智能化的同时&#xff0c;也让保险服务覆盖率有了极大的提升。然而,保险业服务在智能化转型方面仍面临着诸多挑战。 咨询热线统一接入&#xff0c;客户来电不遗漏 保险企业客户不仅体量大…

人类真的与恐龙无缘见面吗?看看雕刻和绘画怎样说

人类真的与恐龙无缘见面吗&#xff1f;看看雕刻和绘画怎样说 恐龙的形象经常出现在电影、电视和网络动画。它们庞大的身躯、凶猛的样子都让人留下深刻的印象。 但是我们今天看到的恐龙都只是出现在屏幕或书籍中&#xff0c;还有在博物馆内恐龙的模型或骨架&#xff0c;那历史上…