Java初学者文档

1、Java 简介

1.1 相关概念

Java

Java是一门面向对象程序设计语言

Java 开发环境

主要了解 JVM、JRE 和 JDK 这三个内容

JVM: java虚拟机

​ 是java跨平台的依赖

JRE: java运行时环境,包含 JVM 和运行时所需要的核心类库

​ 如果需要运行一个 java 程序,只需要安装 JRE 即可

JDK: java 程序开发工具包,包含 JRE 和开发人员使用的工具

​ 如果需要开发一个 java 程序,需要安装 JDK

Java 运行过程

java 没有用编译器直接把 .java 文件翻译成机器语言,而是通过编译器将 .java 文件编译成 .class 字节码文件(这样减少了编译的时间)

然后不同系统的 JVM 将 .class 字节码文件翻译成对应系统可以识别的机器语言

Java 跨平台的原理

针对不同的操作系统,java 提供了不同的 JVM

当程序运行时,java源代码(.java)经过编译器编译后,生成字节码文件(.class)

然后 JVM 将字节码文件(.class)翻译成特定平台下的机器码,使得 java 程序可以在不同平台运行

注意:跨平台指的是编译后的文件(.class)跨平台,而不是源程序文件跨平台

​ java 语言是跨平台的,但是 JVM 不是,不同平台有不同的平台的 JVM

1.2 Java 语言的特点

Java 语言的组成

Java 语言是由 语法规则 和 类库 两部分组成,语法规则确定了编写的规则,类库提升了编写的效率

  • 语法规则:确定 Java 程序的书写规范
  • 类库:提供了一系列由 开发人员或者软件供应商编写的 Java 模块(类),编写 Java 程序时可以直接使用

Java 语言的优点

  • 结构简单
  • 面向对象
  • 平台无关(跨平台)
  • 可靠性(例如:异常处理机制)
  • 安全性
  • 支持多线程

Java 编程工具

常用的有:Eclipse 和 IDEA,我就直接用 IDEA 了

2、环境配置

2.1 配置原因

开发 java 程序,需要使用到 JDK 提供的开发工具,而这些工具在 JDK 安装目录的 bin 目录下

如果直接在命令行使用 JDK 中的命令,系统会报错提示找不到,为什么会找不到呢?

这涉及到计算机查找命令的方式,输入命令后,系统如果找不到命令,就无法执行相应的操作

  • Windows 操作系统是根据 path 环境变量中配置的路径查找命令
  • Linux 操作系统是根据 PATH 环境变量中配置的路径查找命令

开发时为了在任意位置都可以使用 JDK 提供的命令,就需要配置环境变量了

2.2 配置方法

①:在系统环境变量里创建新的变量:JAVA_HOME,其变量值为:JDK的安装目录

​ 例如:我的 JAVA_HOME 的变量值就是 D:\DevTools\Java

②:在系统的 path 环境变量后面添加:%JAVA_HOME%\bin

以前的版本还需要额外配置 jre, 但是我尝试不配置并无什么影响 %JAVA_HOME%\jre\bin

③:早些版本的 JDK 需要设置 classpath 来指定 JRE 去哪里找编译后的文件(.class)

  • 配置了这个之后, JRE 就只会在 CLASSPATH 指定的路径中查找字节码文件(.class)
  • 当然,现在版本的 JDK 可以不配置这个,默认就会在当前工作目录和 JDK 下的 lib 包中查找

​ 当然你想配置也可以,在系统环境变量里创建新的变量:CLASSPATH

​ 为 CLASSPATH 添加变量值:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

​ 这个变量值前的 .; 表示当前路径,如果不配置,则不会在当前路径下寻找

2.3 配置验证

配置完毕之后,重新打开命令行,输入 java 或者 javac,如果出现一系列帮助说明则是配置成功

3、基础相关

3.1 你好,世界

在没有开发工具的情况下,步骤比较繁琐:

  • ① 创建文件:新建文本文档,将名字更改为 HelloWorld.java

  • ② 编写程序:

    java 复制代码
    // 定义的类,类名要跟文件名一致
    public class HelloWorld{
        /*
            main方法是程序的入口
            代码的执行是从main方法开始的
        */
        public static void main(String args[]){
            // 输出语句
            System.out.println("Hello,World");
        }
    }
  • ③ 运行程序:打开命令行,进入 HelloWorld.java 所在的目录,输入 javac HelloWorld.java

系统会在 HelloWorld.java 所在的目录生成 HelloWorld.class 文件,继续输入 java HelloWorld

系统会在命令行打印出:Hello,World

如果有了开发工具,创建一个 java 项目时会自动生成一个类,直接运行就可以了

标识符

只以 字母、下划线、$ 开头

只能由 字母、下划线、数字、$ 这四种东西混合而成

数据类型

Java 数据类型分为 基本数据类型 和 引用数据类型

  • 基本数据类型:byte、short、int、long、float、double、char、boolean
  • 引用数据类型:数组、类、接口这些都属于引用类型
基本数据类型 字节 默认值
byte 1 0
short 2 0
int 4 0
long 8 0L
float 4 0.0f
double 8 0.0d
char 2
boolean false

标识符注意:char 类型在内存中以整数的形式存储的,而且不允许为 负值

变量

变量必须先声明,再使用

变量一般是由 类型+标识符+可选的一个初始值 来定义

java 复制代码
// 先声明后赋值使用
int name;
name = "赤牙";

// 定义的时候就赋初始值
int num = 100;

// 错误示范:没有声明就使用
// age = 18;

注释

注释不会参与程序的运行,仅仅起到说明的作用

注释分类:

  • 单行注释:// 注释内容
  • 多行注释:/* 注释内容 */
  • 文档注释(可以通过 javadoc 命令生成文档):/** 注释信息 */
3.2 类型转换

类型转换分为两种:自动类型转换 和 强制类型转换

需要注意的两点

  • byte、short 进行运算时,结果会自动转化成 int 类型

  • 而且有多个类型参与运算时,结果会转化成最高的那个类型

自动类型转换

自动类型转换的条件:两个类型兼容、要转换的类型的范围要比原类型大

java 复制代码
// 定义一个 short 类型的变量
short num = 10;

// short 类型自动转换为 int 类型
// 由小到大
int i = num;

强制类型转换

要转换的类型比原类型小,转换的时候就需要强制类型转换

但是强制类型转换可能会出现一些问题,例如下面这个例子的精度问题

java 复制代码
// 定义一个 double 类型的变量
double num1 = 100.23d

// 将 double 类型强制转换为 int 类型
// 强制类型转换可能会出现精度问题,num2 变成了 100
int num2 = num1;
3.3 字符串

字符串需要了解一下 String、StringBuffer、StringBuilder

String 是不可变字符串

String 的定义和实例的生成

java 复制代码
// 这里有三种方式
// 1. 先声明变量,后赋值
String s1;
s1 = "A";

// 2. 通过构造方法
String s2 = new String("B");

// 3. 声明变量的时候直接赋值
String s3 = "C";

字符串常量池

Java 内存中存在一个字符串常量池,主要作用就是节省内存,提高使用字符串的效率

java 复制代码
// String 是 final 修饰的,其定义的值会存储在 private final byte[] value; 中
// 给 s1 赋值时发现常量池中没有 "Hello",就会先在字符串常量池中声明一个对象叫 "Hello"
// 然后 s1 的引用指向了 "Hello" 的地址
// 给 s2 赋值时,发现常量池已经有了 "Hello" 了,就直接也把 s2 的引用也指向了 "Hello" 的地址
// 所以这两行执行时只会创建一个对象:就是字符串常量池里面的 "Hello"
String s1 = "Hello";
String s2 = "Hello";

// 下面两行在编译时就会拼接好字符串了,然后再去常量池中找,没有就创建
// 而前面的 s1 已经在常量池中创建了 "Hello" 了
// 所以这两行也只会创建一个对象:就是字符串常量池里的 "world"
String s3 = "He" + "llo";
String s4 = "wo" + "rld";

// new Xxx() 的话肯定会先会创建一个对象
// 但是因为 s1 在常量池中有对应的引用,就直接引用了
// 所以这里只会创建一个对象:就是 new 的那个对象
String s5 = new String(s1);

// 采用变量名进行拼接时,实际上是通过 new StringBuilder().append().toString() 这样的操作来实现的
// 所以这里首先创建了一个 new StringBuilder() 对象用来拼接
// 然后因为已经有了 s3 s4 ,所以直接 append() 拼接,StringBuilder() 的 append() 会返回这个对象本身,所以不会创建新对象
// 拼接完成后会调用 toString() 方法,这个方法中会执行 new String() 的操作,所以会产生一个对象,但不会保存在常量池中
// 所以在上面的基础上,这里共创建了 2 的对象
String s6 = s3 + s4;

// 这里也是拼接,所以同样是通过 new StringBuilder() 进行
// new 了两下,所以会再创建两个对象
// "a" 和 "b" 在常量池中并不存在,所以也会创建两个对象
// 拼接完成后会调用 toString() 方法创建一个对象 new String()
// 所以这里一共会产生 6 个对象
String s7 = new String("a") + new String("b");

// xxx.intern() 会在字符串常量池创建一个对象,并把 xxx 的引用指向这个字符串,但是如果常量池存在这个字符串,就只改变引用
// 这里 s7 的值是 "ab",常量池没有,所以会在常量池创建一个对象:"ab",并把 s7 重新指向常量池里面这个 ab 的地址
s7.intern();

// 这里 s7 == s8
String s8 = "ab";

String 相关方法

java 复制代码
// 1. 类型转换
int num = 100;
// 方法一:通过 valueOf() 方法,将特定类型转化成字符串
String s1 = String.valueOf(num);
// 方法二:通过加一个空字符串
String s2 = num + "";

// 2.根据下标截取一个字符
String s = "HelloWorld";
char ch = s.charAt(0);  // H

// 3.将字符串转化成字节数组
String s = "HelloWorld";
byte[] bytes = s.getBytes();
System.out.println(bytes[0]);  // 72

// 4.将字符串转化成字符数组
String s = "HelloWorld";
char[] ch = s.toCharArray();
System.out.println(ch[0]);  // H

// 5.判断字符串的值是否相等
String s1 = "HelloWorld";
String s2 = "HELLOWORLD";
// 直接比较
System.out.println(s1.equals(s2));  // false
// 忽略大小写
System.out.println(s1.equalsIgnoreCase(s2));  // true

// 6.判断字符串是否以 xxx 开头或者结尾
String s = "HelloWorld";
System.out.println(s.startsWith("he"));  // false
System.out.println(s.endsWith("rld"));  // true

// 7. 比较两个字符串大小,一般是排序的时候才会用到,通常也只是看结果 >0、=0 还是 <0 来判断大小
// 规则是:拿到较短的字符串的长度,然后循环比较每个字符的 ASCII 值
// ASCII 值一样就开始下一轮循环,不一样就直接返回对应 ASCII 值的差
// 循环结束都没判断出来,就返回两个字符串长度的差
String s1 = "abc";
String s2 = "abcd";
System.out.println(s1.compareTo(s2) > 0); // false,说明 s1 < s2

// 8.搜索字符串中【指定的字符串】第一次出现的位置和最后一次出现的位置
String s = "HelloWorld";
System.out.println(s.indexOf("o"));  // 4
System.out.println(s.lastIndexOf("o"));  // 6

// 9.截取字符串
String s = "HelloWorld";
// [3,s.length)
System.out.println(s.substring(3));  // loWorld
// [3,5)
System.out.println(s.substring(3,5));  // lo

// 10.替换字符串
String s = "HelloWorld";
s = s.replace("o","w");
System.out.println(s);  // HellwWworld

// 11.去掉前后空格
String s = "   HelloWo rld   ";
String s1 = s.trim();
System.out.println(s);   // 输出前后会有空格:    HelloWo rld   
System.out.println(s1);  // 输出前后没有空格:HelloWo rld

判断两个字符串是否相等

java 复制代码
String s1 = "Hello";
// s2,s1 指向的是常量池中同一个地址
String s2 = s1;
String s3 = new String(s1);

// == 比较地址值
System.out.println(s1 == s2);  // true 地址值一样
System.out.println(s1 == s3); // false 地址值不一样

// .equals() 比较内容
System.out.println(s1.equals(s2));  // true
System.out.println(s1.equals(s3));  // true

StringBuffer 和 StringBuilder

这两个是可变字符串,用于频繁的转化字符串

与 String 不同的是,它们对字符串进行操作的时候不会产生新的对象,而是在本来的对象上操作

StringBuilder 线程不安全,所以在不考虑线程安全的情况下,StringBuilder 的运行效率可能会更高

java 复制代码
String s1 = "Hello";
StringBuilder s = new StringBuilder(s1);

// 在字符串后添加内容
s.append(1).append("H");

System.out.println(s1);  // Hello
System.out.println(s.toString());  // Hello1H

// 在字符串指定位置插入一些其他字符串
s.insert(1, "abc");  // Habcello1H
3.4 数组

数组的特点

  • 存储多个相同类型的值
  • 所有元素所占的空间是连续的
  • 元素的访问依赖于下标,且下标从 0 开始
  • 数组在 java 中实际上是一个对象

数组的定义和使用

java 复制代码
// 静态初始化,只能在数组定义时使用
int[] arr1 = {1,2,3,4,5};

// 动态初始化,指定了数组的长度,但是可以在任意地方使用
int[] arr2 = new int[5];

// 数组的复制
// 方法一:循环赋值,新数组需要先指定长度
int[] arr3 = new int[arr1.length];
for (int i = 0; i < arr1.length; i++){
	arr3[i] = arr1[i];
}

// 方法二:调用 Arrays 的方法,这两个方法都会返回新数组
// copyOf(src, length) 第二个参数是新数组的长度,当新数组的长度 > arr1 长度时,多的部分会用 0 填充
int[] arr4 = Arrays.copyOf(arr1, 2);
// copyOfRange(src, start, end) 从当前数组的第 start 位置开始复制,新数组的长度是 end-start,多的部分会用 0 填充
int[] arr5 = Arrays.copyOfRange(arr1, 3, 10);

// 方法三:方法二实际上还是调用的这个方法
// arraycopy(原数组,从哪个位置开始复制,目标数组,目标数组起始位置,复制元素的个数)
int[] arr6 = new int[10];
System.arraycopy(arr1, 0, arr6, 2, 2);

// 方法四:调用 Object 的 clone 方法
int[] arr7 = arr1.clone();

运算符

条件语句

  • if - else
  • switch

switch 语句注意事项

  • case 值不可重复,case 的值必须是与表达式的类型兼容的常量
  • 如果 case 中没有满足条件的,那么会执行 default 语句块的内容
  • case 中的语句记得添加 break;
  • 如果 case 没有添加 break; 语句,那么会从匹配项依次执行,直到遇到 break; 语句
java 复制代码
// 从键盘录入一个数据
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
switch (num){
    case 1:
        System.out.println("A");
    case 2:
        System.out.println("B");
    case 3:
        System.out.println("C");
        break;
    case 4:
        System.out.println("D");
        break;
    default :
        System.out.println("其他情况");
        break;
}

// 运行上面的例子
//    输入:5      控制台会显示:其他情况
//    输入:4      控制台会显示:D
//    输入:1      控制台会显示:A B C (这三个中间有换行)

循环语句

  • while:先判断,再执行循环体
  • do...while:先执行一次循环体,再进行判断
  • for:循环遍历,简单灵活
  • forEach:注意:forEach 只能遍历,不能赋值
java 复制代码
// 运用四种循环输出 List 集合中的数据
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

// while 循环
int i = 0;
while (i < list.size()){
    System.out.println(list.get(i));
    i++;
}

// do...while 循环
int j = 0;
do{
    System.out.println(list.get(j));
    j++;
}while (j < list.size());

// for 循环
for (int i = 0; i < list.size(); i++){
    System.out.println(list.get(i));
}

// forEach 循环
for (String s :list){
    System.out.println(s);
}

中断语句

  • break; 退出循环
  • continue; 跳出本次循环,开始下一次循环
  • return; 返回方法的返回值,并且之后的语句不会执行,除了异常处理的 finally{...} 语句块

4、面向对象

4.1 类和对象

类是具备某些 **共同属性和行为 **的实体的集合,可以把它当成是一种模板

一般情况下,类中会定义一些属性和方法,其中属性用来描述这个类对应实体的属性,方法用来描述实体的行为

下面的例子:定义了一个人的类,人都有姓名和年龄的属性,同时也具备吃饭和睡觉的行为

java 复制代码
public class Person {
    
	// 属性
    private String name;
    private Integer age;
    
    // 行为
    public void eat() {
        System.out.println("吃饭");
    }
    
    public void sleep() {
        System.out.println("睡觉");
    }
}

// 创建一个人类的对象:就相当于以 Person 类为模板,创造了一个人
// 每个人都是与众不同的,所以他们的属性是独一无二的(私有的)
// 每个人都会吃饭睡觉,所以他们的行为是公共的
Person p = new Person();
p.eat();
p.sleep();

类的设计技巧

  • 属性私有化,提供方法供外部使用
  • 不要使用过多的基本数据类型
  • 数据要初始化
  • 类功能要分解,最好让每个类都实现单一职责

构造方法

构造方法与类名一样,但是没有返回值

没有写构造方法时,系统会提供一个默认的无参的构造方法,如果我们写了构造函数,默认的就不会提供了

构造函数不能通过对象直接调用

构造函数的作用:产生对象(对象的产生必须通过构造函数来完成),可以在构造对象的过程中给对象的属性赋值

方法重载

  • 方法重载和参数的个数、类型、顺序相关,与返回值无关
  • 多个功能相似的方法可以使用重载机制,方便开发
  • 给重载方法传递参数的时候要注意参数类型的自动提升

静态和非静态

  • 被 static 修饰的属性和方法属于类,它们不依赖对象,可以通过类名直接调用

  • static 变量相当于全局变量,生成新的对象的时,不会产生新的 static 变量的拷贝

  • 类第一次初始化的时候,static 就会初始化,而那些不是 static 的属性则会在创建对象的时候初始化

  • 静态方法只能调用静态方法和静态变量,不能访问 this 和 super

this

  • 一个方法需要引用调用它的那个对象,this 可以指向这个对象
  • this 可以在类的内部调用当前对象的属性和方法
  • 在构造函数中用 this 调用其他构造函数时,this 需要写在第一行

super

  • 可以通过 super 调用父类的属性、方法、构造方法
  • 在子类构造函数中通过 super 调用父类构造方法时,super 必须在第一行
  • 它不是对象的引用,只是一个指示编译器调用父类方法的关键字,this 是代表当前对象的一个引用

类成员的初始化

初始化的方式:

1- 定义的时候直接初始化

2- 构造函数中初始化

3- 初始化块

4- 静态变量可以通过静态初始化块

可变参数列表

java 复制代码
String s1 = "A";
String s2 = "BB";
String s3 = "CCC";

String[] arr = new String[]{s1,s2,s3,"DDDD"};

show(arr);
show(s1,s2,s3);

public static void show(String...str){
    for (String s : str){
        System.out.print(s + " ");
    }
    System.out.println();
}

封装类

常用的封装类:Byte、Short、Integer、Long、Float、Double、Character、Boolean

有时候需要将基本数据类型转换成对象,所以封装类就产生了,封装类都是 final 修饰的,不能被继承

封装类提供了自动装箱和拆箱的操作
提供了 String 到 基本数据类型的转换方法

java 复制代码
// 装箱
int num1 = 100;
Integer i1 = Integer.valueOf(num1);

// 自动装箱
Integer i2 = 200;

// 拆箱
Integer i3 = new Integer(300);
int num2 = i3.intValue();

// 自动拆箱
Integer i4 = new Integer(400);
int num3 = i4;

类访问权限

  • public:所有包中的类都可以访问
  • private:只有本类可以访问
  • protected:同一个包中的类可以访问,不同包的类访问时,必须是这个类的子类
  • 包访问权限:同一个包的类可以访问

类之间的依赖关系

依赖

一个类的对象的行为实现需要依赖另一个类的对象

体现就是 BankCard 类作为参数在 Me 类的 saveMoney() 中被使用

java 复制代码
// 例如,存钱的操作,就需要依赖银行卡类
public class Me{
    public void saveMoney(BankCard card,double money){...}
}

关联

两个类存在一种强依赖关系,双方关系平等

体现就是 Address 类作为属性出现在 Student 类中

java 复制代码
// 学生类知道 Address 类的存在
public class Student{
    Address addr;
}

聚合:关联关系的特例
组合
继承

  • 继承是父类和子类之间共享数据和方法的机制
  • 继承具有传递性
  • 单继承(extends)、多继承(接口继承)
  • 类之间只能单继承,当类和类之间存在 is - a 的关系时,可以使用继承

**继承的作用:**提高代码重用率,使软件系统具有开放性,更好的进行抽象和分类

继承的设计原则

将公共操作和公共属性放在父类中

使用继承时要体现出 is-a 的关系

方法重写时,不要改变预期的行为

除非继承的方法有特别的意义,否则不要使用继承,可以使用组合和代理

方法重写

对于相同的方法名,子类和父类可以通过不同的实现方式体现出区别

  • 发生在子类和父类之间

  • 子类覆盖父类方法时,方法名要保持一致,返回值类型可以不一致,但是必须是父类该方法返回值的子类

    java 复制代码
    class A{}
    
    class B extends A{}
    
    class Father{
        public A show (){}
    }
    
    class Son extends Father{
        // 重写父类的方法时,下面两者都可以
        public A show(){}  // 直接重写
        public B show(){}  // 返回值类型[B]是父类的 show() 方法返回值类型[A]的子类
    }
  • 子类重写方法时,访问权限不能低于父类方法的访问权限

  • 不能重写父类的私有方法

引用类型的转型问题

  • 父类引用指向子类对象,可以无条件转型,但是调用方法时,只能调用父类的方法,方法的实现最终还是子类的实现
  • 子类对象指向父类引用,需要强制类型转换,
java 复制代码
public static void main(String[] args) {
    
    SuperMan sm = new SuperMan();
    // 父类引用指向子类对象
    Person p = sm;
    p.show();
    
    // 子类对象指向父类引用,运行时候会报错
    // Person p = new Person();
    // SuperMan sm = (SuperMan) p;
    // sm.grow();
    // sm.show();
    
    // 强制将子类对象的引用转化为子类型
    Person p2 = new SuperMan();
    SuperMan sm2 = (SuperMan) p2;
    sm2.show();
    sm2.grow();
}

class Person{
    public void show(){
        System.out.println("person");
    }
}

class SuperMan extends Person{
    public void show(){
        System.out.println("superman");
    }
    public void grow(){
        System.out.println("fly");
    }
}
3.10 垃圾回收器

内存泄漏:当一些已经分配出去的内存得不到及时回收或者无法回收,导致系统运行速度下降或者瘫痪的现象叫内存泄漏。

为了解决内存泄漏的问题,**垃圾回收机制(GC)**就出现了。

GC 的核心是垃圾回收算法,这个算法就两个作用

  • 一个是发现无用的对象
  • 另一个是回收被这些无用对象占用的内存空间,使得该空间可以被程序再次使用

垃圾回收机制只能回收内存资源,对于数据库、l/O等物理资源无法回收。

JRE 会提供一个后台线程进行监测和控制,它会回收堆内存中无用的对象,同时它也可以整理内存记录碎片,碎片整

理会将那些空闲的堆内存移动到堆的另一端以供新的对象使用。

  • 内存记录碎片:分配给对象内存块之间的空闲内存区,就是内存记录碎片

  • 碎片出现的原因:创建对象和垃圾回收机制回收对象时,会在堆内存留下空白的内存块。

GC 的特点:

  • 精确性
    • 能精确的标记活着的对象,这个可以确保对象什么时候进行回收
    • 能精确的定位对象之间的引用关系
  • 自动回收内存,提高效率
  • 不可预知,就是你也不知道它什么时候会被回收
  • 由于精确性,使得程序性能受到影响

垃圾回收机制依赖于垃圾回收器,垃圾回收器

  • 刚开始是GMS(并行标记/清除垃圾回收器)
  • 后来就出现了G1垃圾回收器
  • 以后可能会出现性能更好的垃圾回收器(例如还在实验阶段的ZGC)
3.4 修饰符

java 中的属性和方法有不同的访问权限,可以通过访问权限修饰符对其进行区分

  • public 修饰的属性和方法,所有类都可以访问
  • private 修饰的属性和方法,只有同一个类中可以访问
  • protected 修饰的属性和方法,同一个包中的类、不同包的子类可以访问
  • default 修饰的属性和方法,同一个包中的类可以进行访问
3.5 类型转换

自动类型转换:把一个表示数据范围小的数值赋值给另一个表示数据范围大的数值

  • byte -- short -- int -- long -- float -- double

  • char -- int -- long -- float -- double

强制类型转换:把一个表示数据范围大的数值赋值给另一个表示数据范围小的数值

目标数据类型 变量名 = (目标数据类型) 数值或变量;

java 复制代码
public class Hello {
    public static void main(String[] args) {

        // 自动类型转换
        byte b = 10;
        short s = b;
        int i = s;
        long l = i;
        float f = l;
        double d = f;

        System.out.println(d);

        // 强制类型转换
        double dd = 10.0;
        
        int ii = (int) dd;
        
        System.out.println(ii);
    }
}

装箱和拆箱

了解装箱和拆箱之前首先需要知道包装类,所谓的包装类,就是基本数据类型封装成为对应的对象类。

为什么这么做呢?

因为普通的基本数据类型无法调用方法,而通过包装类,则可以定义方法操作数据,比较灵活

java 复制代码
public class Hello {
    public static void main(String[] args) {
        
        // 将 int 类型的数据转换成 String 类型的数据
        int num1 = 100;
        // 方法一:添加一个""
        String s1 = num1 + "";
        // 方法二:通过 String 类的 valueOf() 方法
        String s2 = String.valueOf(num1);
        System.out.println(s1);

        // 将 String 类型的数据转化成 int 类型的数据
        String s3 = "1230";
        // 方法一:通过 Integer 类的 valueOf() 方法
        Integer integer = Integer.valueOf(s3);
        int num2 = integer.intValue();
        
        // 方法二:通过 Integer 类的 parseInt() 方法
        int num3 = Integer.parseInt(s3);
    }
}

装箱:将基本数据类型转化成对应的包装类

拆箱:将包装类转化成基本数据类型

java 提供了自动装箱、拆箱的机制,方便了写代码

java 复制代码
public class Hello {
    public static void main(String[] args) {
        // 装箱
        int num1 = 100;
        Integer i1 = Integer.valueOf(num1);

        // 自动装箱
        Integer i2 = 200;

        // 拆箱
        Integer i3 = new Integer(300);
        int num2 = i3.intValue();
        
        // 自动拆箱
        Integer i4 = new Integer(400);
        int num3 = i4;
    }
}
3.6 数组

数组:一种用于存储多个相同类型数据的存储模型

数组初始化:Java 中的数组必须先初始化,然后才能使用

​ 所谓初始化,就是为数组中的数组元素分配内存空间,并为每个元素赋值

java 复制代码
public class Hello {
    public static void main(String[] args) {
        
        // 动态初始化,指定长度,系统会赋默认值
        int[] arr1 = new int[5];

        // 静态初始化,指定内容
        int[] arr2 = {1,2,3,4,5};
    }
}

不同数据类型的默认值:

  • 整数默认值:0
  • 浮点数默认值:0.0
  • 字符默认值:空字符
  • 布尔值默认值:false
  • 引用数据类型默认值:null
3.7 方法

所谓方法,就是为了完成某一个功能的代码集合

方法需要先创建,后使用。方法是供外部进行调用的。

方法类别很多,主要在于修饰符类型、返回值、参数,静态还是非静态,很常见而且很简单,这里就不写了

对于方法,比较重要的就是区别一下 重写和重载

重写:两个类存在继承关系,子类可以重写父类的非私有方法,重写时,需要保证修饰符类型,返回值类型,方法名,方法参数一样

重载:一个类中的多个的多个方法,其方法名相同,参数类型或者个数不同,与返回值无关

3.8 final

final 可以修饰类、方法、变量

final 修饰的类,不能被继承

java 复制代码
// final 修饰的父类
public final class Father{
    public void show(){
        System.out.println("父亲");
    }
}

// 下面这样会报错
// public class Son extends Father{ }

final 修饰的方法不能被子类重写,但是可以重载

java 复制代码
public class Father{
    // 父类中 final 修饰的方法
	public final void show(){
		System.out.println("父亲");
	}
}

public class Son extends Father{
    // 下面这样写会报错
    // public void show(){ }
    
    // 正确写法: 方法重载,变相重写父类被 final 修饰的方法
	public void show(String name){
		System.out.println("儿子" + name);
	}
}

public class Hello{
    public static void main(String args[]){
        // 创建子类对象
        Son son = new Son();
        
        // 调用父类的方法
        son.show();
        
        // 调用子类重载的方法
        son.show("小白");
    }
}

final 修饰的变量必须初始化,可以在声明的时候直接初始化,也可以在构造函数中初始化

java 复制代码
public class Hello {

    // final 修饰成员变量,声明的时候初始化
    public final int num1 = 100; // 基本数据类型,其数值无法变化
    public final String s1 = "我是s1"; // 引用类型,其地址无法变化

    // 通过构造函数初始化
    public final int num2;
    public final String s2;


    public Hello(){
        this.num2 = 200;
        this.s2 = "我是s2";
    }

    // final 修饰局部变量,调用方法的时候会赋值,而且方法中其数值也无法发生变化
    public void m1(final int num3){
        // num3 = 300; 这样写不对,会报错
    }
    public void m2(final String s3){
        // s3 = "我是s3"; 这样写不对,会报错
    }

    // main() 方法
    public static void main(String[] args) {
        
        // final 修饰局部变量,必须直接赋值,而且赋值后无法修改
        final int num4 = 400;
        final String s4 = "我是s4";

        Hello h = new Hello();
        System.out.println(h.num1);
        h.m1(100);
        h.m2("我是s3");

        System.out.println(num4);
        System.out.println(s4);

        // 引用类型的地址不能改变
        final StringBuilder s5 = new StringBuilder("我是s5");
        s5.append("哈哈"); // append() 方法返回的是对象本身,所以地址值并没有发生变化
        System.out.println(s5);
        
        // 上面那个例子不太明显,再举一个
        // 在外部定义一个 final 修饰的类:World
        World w = new World(1,"世界");
        System.out.println(w);
        
        // 下面这一行,w 的地址发生变化,所以会报错
        // w = new World(1,"月亮");
        
        // 下面修改 w 的属性,但是 w 的地址没有发生变化,不会报错
        w.setName("地球");
        System.out.println(w);

    }
}
3.9 static

static 可以修饰成员方法、成员变量

总结一下就是,静态的只能访问静态的

java 复制代码
public class Hello {

    // static 修饰的成员变量
    public static int num1;
    // 普通的成员变量
    public int s1;

    // static 修饰成员方法
    public static void m1(){
        System.out.println("我是m1");
    }
    
    // 普通方法
    public void m2(){
        System.out.println("我是m2");
    }

    public static void main(String[] args) {

        // 直接通过 类名.属性名 的方法访问静态变量
        System.out.println(Hello.num1);
        System.out.println(Hello.s1);

        // 通过 类名.方法名 访问静态方法
        Hello.m1();
        
        // 访问普通变量和普通方法,需要创建对象,通过对象访问
        ...
    }
}

== 和 equals() 的区别

== 比较的是地址值是否一致
equals() 一般需要进行重写,用来判断值是否相等

Object 类是所有类的祖宗,所以说,所有类的 equals() 方法都继承自 Object 类,而 Object 类的 equals() 方法如下:

java 复制代码
// Object 的 equals() 方法
public boolean equals(Object obj) {
        return (this == obj);
}

可以看出,如果不重写 equals() 方法,那么默认比较的就是地址值

对于基本数据类型而言,通过 == 比较即可判断内容是否一样

对于引用数据类型而言,需要重写 equals() 方法实现想要的功能,String 类就重写了 equals() 方法

java 复制代码
// String 类的 equals() 方法
public boolean equals(Object anObject) {

    // 首先判断两个对象的地址值是否一样,地址值一样那么内容肯定也一样
    if (this == anObject) {
        return true;
    }
    
    // 如果地址值不一样,判断另一个对象是否也是 String 类型的数据,如果不是,那么返回 false
    // instenceof 表示判断 anObject 是否是 String 的实例,返回 false 或者 true
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        // 判断长度是否一样,长度不一样肯定返回 false
        if (n == anotherString.value.length) {
            
            // 长度一样,比较每个字符是否一样
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

与 String 类一样,我们也可以重写 equals() 方法实现对象内容的比较

java 复制代码
// 定义一个类
public class World {
    
    // 随便写俩属性
    private String name;
    private int age;

    // 构造方法
    public World(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // get/set
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    // 重写 equals() 方法
    @Override
    public boolean equals(Object o){
        // 判断地址是否相等
        if (this == o){
            return true;
        }
        
        // 对属性进行比较
        World w = (World) o;
        return this.getName().equals(w.getName()) ? this.getAge() == w.getAge() : false;
    }
}
5、面向对象
1. 封装

private

封装时需要使用到 private关键字,将代码封装起来,保护成员不被其他类使用,提高了代码的复用性和安全性

private 关键字可以修饰成员变量和成员方法,被 private 修饰的成员只能在本类中访问

针对被 private 修饰的成员变量,如果需要给其他类使用,我们要提供相应的 public 方法

  • getXxx():在其他类中用于获取成员变量的值

  • setXxx():在其他类中用于设置成员变量的值

带有继承关系的构造方法执行顺序

优先执行父类和子类的静态代码块,再执行父类代码块和无参构造函数,最后执行子类的代码块和无参构造函数(带参构造函数)

java 复制代码
public class Father {

    private String name;

    public Father(){
        System.out.println("Father 无参构造");
    }

    public Father(String name){
        System.out.println("Father 有参构造");
    }

    static {
        System.out.println("Father 静态代码块");
    }

    {
        System.out.println("Father 代码块");
    }
}

public class Son extends Father {

    private String name;

    public Son(){
        System.out.println("Son 无参构造");
    }

    public Son(String name){
        System.out.println("Son 带参构造");
    }

    static {
        System.out.println("Son 静态代码块");
    }

    {
        System.out.println("Son 代码块");
    }
}

public class Hello {

    public static void main(String[] args) {
        Son s = new Son();
        Son son = new Son("123");
    }
}

/*
    Father 静态代码块
    Son 静态代码块
    Father 代码块
    Father 无参构造
    Son 代码块
    Son 无参构造
    ------------
    Father 代码块
    Father 无参构造
    Son 代码块
    Son 带参构造

*/
2. 继承

extends

继承使子类具有父类的属性和方法,子类也可以有自己的属性和方法,同时子类可以对父类的方法进行重写以达到覆盖的目的

继承使得代码复用性提高,但是增加了耦合度,但世界就是这样,有好的一面就有坏的一面嘛

特点

  • 构造方法访问顺序,子类的构造方法默认会访问父类的构造方法
  • 成员方法访问顺序,通过子类对象访问方法,优先在子类中寻找,找不到再在父类中找
  • 变量访问顺序,优先在当前方法中寻找,再在子类成员变量中寻找,最后在父类成员变量中寻找
java 复制代码
public class Father {

    int age = 40;

    public Father(){
        System.out.println("Father 无参构造");
    }

    public Father(int age){
        this.age = age;
        System.out.println("Father 有参构造");
    }

    public void show(){
        System.out.println("我是 Father");
    }

}

public class Son extends Father {

    int age;

    public Son(){
        System.out.println("Son 无参构造");
    }

    public Son(int age){
        this.age = age;
        System.out.println("Son 带参构造");
    }

    @Override
    public void show() {
        int age = 10;
        
        // 这个 age 遵循就近原则的顺序
        System.out.println(age);
        
        // this.age 表示的是当前对象的 age
        System.out.println(this.age);
        
        // super.age 表示的是父类对象的 age
        System.out.println(super.age);
        System.out.println("我是 Son");
    }
}

public class Hello {

    public static void main(String[] args) {
       Son s = new Son(123);
       s.show();
    }
}
3. 多态

多态,即同一个对象在不同时刻表现出来的不同形态

多态的前提:

  • 有继承或者实现关系

  • 有方法重写

  • 有父类引用指向子类对象,即 Fu f = new Zi();

多态提高了程序的扩展性,但是不能实现子类特有的功能。

为了解决弊端,提出了多态的转型

  • 多态默认是向上转型,即父类引用指向子类对象
  • 向下转型,将父类引用强转为子类对象
java 复制代码
public class Animal {

    String name;

    public Animal(){
        System.out.println("父类无参");
    }

    public Animal(String name){
        this.name = name;
    }

    public void eat(){
        System.out.println("动物会吃饭");
    }
}

public class Cat extends Animal{
    String name;
    String type;

    public Cat(String name,String type){
        this.name = name;
        this.type = type;
    }

    @Override
    public void eat() {
        System.out.println(this.name + "吃" + this.type);
    }

    public void run(){
        System.out.println(this.name + "跑的飞快");
    }
}

public class Hello {

    public static void main(String[] args) {
        
        // 多态
        // 向上转型,父类引用指向子类对象
        // 此时子类独有的方法无法访问
        Animal animal = new Cat("猫","老鼠");
        animal.eat();

        // 向下转型,强转父类引用
        // 可以访问子类独有的方法
        Cat cat = (Cat) animal;
        cat.run();
    }
}

多态访问成员变量:

  • 编译的时候看左边,也就是父类
  • 运行的时候也看左边

多态访问成员方法:

  • 编译的时候看左边,也就是父类,

  • 运行的时候看右边,因为子类重写了父类的方法

4. 内部类

内部类即为类中类,分类也不少,看看把,感觉也就匿名内部类有点用

  • 成员内部类:顾名思义,相当于正常类的一个成员变量
  • 局部内部类:顾名思义,相当于正常类的一个局部变量
  • 匿名内部类:顾名思义,没名字的内部类

成员内部类

java 复制代码
public class World {

    private int age = 100;

    private class Inner{
        private String name = "abc";
        public void show(){
            System.out.println(name + age);
        }
    }

    // 可以看作跳板,外面的类通过这个方法访问到内部类
    public void show(){
        Inner i = new Inner();
        System.out.println(i.name);
        i.show();
    }
}

public class Hello {
    public static void main(String[] args) {
        World w = new World();
        w.show();
    }
}

局部内部类

java 复制代码
public class World {

    private int age = 100;

    public void show(){
        int id = 1;
        class Inner{
            String name = "abc";
            public void show(){
                System.out.println(id + "-" + age + "-" + name);
            }
        }
        Inner i = new Inner();
        i.show();
    }
}

public class Hello {
    public static void main(String[] args) {
        World w = new World();
        w.show();
    }
}

匿名内部类。我感觉这个一般接口方面用的多

java 复制代码
// 定义一个接口
public interface Animal {
    void eat();
}

// 定义一个接口实现类
public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

// 定义一个接口操作类
public class ShowAnimal {
    public void show(Animal a){
        a.eat();
    }
}

// 测试
public class Hello {
    public static void main(String[] args) {
        
        // 不使用匿名内部类的写法
        ShowAnimal s = new ShowAnimal();
        Animal a = new Cat();
        s.show(a);

        // 使用匿名内部类
        // 可以不用创建接口实现类 Cat
        ShowAnimal showAnimal = new ShowAnimal();
        showAnimal.show(new Animal() {
            @Override
            public void eat() {
                System.out.println("自己写");
            }
        });
    }
}
5.抽象类和接口

抽象类不能产生对象,也就是说它不能 new

抽象类可以包含抽象方法,也能有普通方法,抽象方法不能实现

抽象类专门用来被其它类继承,它的具体实现体现在子类中

可以使类的抽象性更明确,将公共方法向上移动

接口,描述类的功能而不具体实现,它使得处于不同层次,互不关联的类可以具有相同的行为

Java 通过接口实现多继承

instanceof 关键字可以确定某个对象的类是否实现了接口

抽象类和接口的区别

  • 定义的关键字不同,一个是 abstract,另一个是 interface
  • 抽象类中可以有抽象方法,也可以有普通方法,接口中只能有抽象方法
  • 抽象类中可以定义成员变量,接口中只能定义常量,并且必须在定义的时候赋值
  • 一个类只能继承一个抽象类,但可以实现多个接口
  • 抽象了常用于继承中的 is - a 关系,接口往往表示类功能的附加

相同之处:

  • 都不能实例化对象
  • 都可以定义抽象方法
6、异常
1. 异常体系

异常,即程序出现了不正常的情况。

java 通过面向对象的方法来处理异常,在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。

拋出(throw)异常:我们把生成异常对象,并把它提交给运行时系统的过程

捕获(catch)异常:运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象的过程

异常的体系:

Throwable

  • Error:程序无法处理的错误

  • Exception:程序可以处理的异常

    • 运行时异常(RuntimeException):++代码本身出现了问题但是你没发现++,控制台会报错,根据系统提示修改即可
    • 检查异常(checkedException):简单来说就是++写代码的时候就报错++,这种错误必须处理,否则程序无法编译
2. 异常处理方式

JVM 处理:JVM 处理的是运行时异常,也就是你写的时候没报错,运行的时候报错,简言之就是你没发现错误

​ JVM 会把异常的详细信息显示在控制台,同时终止程序

复制代码
			  这个时候,需要注意的是异常语句之后的代码是不会执行的

人为处理:try...catch...finally 语句:这个语句不会终止程序

复制代码
											 			try 语句按顺序执行,发现异常就跳转到 catch

​ catch 语句匹配异常类型,可以有多个,但必须先写子类异常

​ 无论是否有异常,finally 语句必然会执行

​ e.printStackTrace(); 会在控制台显示异常详情

​ throws :对于处理不了的异常,可以在方法名之后通过 throws 语句抛出异常,由上层调用的方法解决

3. 自定义异常

创建自定义异常类继承 Exception,实现其构造方法,在其他类中通过 throw new 自定义异常名 (); 使用

同时,方法名后需要 throws 自定义异常名来进行抛出

java 复制代码
public class MyException extends Exception{
    public MyException(){
        super();
    }

    public MyException(String msg){
        super(msg);
    }
}

public class Hello {
    public static void main(String[] args) throws World {
        try {
            int a = 10 / 0;
            System.out.println(a);
        } catch (Exception e){
            throw new World("不能为0");
        }
    }
}
7、集合
7.1、集合的用途

数组可以保存一组数据,但是数组的尺寸是固定的,由于写代码的时候不知道需要多少对象,于是数组尺寸的固定限制了它的使用

集合正好可以弥补数组的不足之处

基本的集合:List、Set、Queue、Map

List、Queue、Set 都继承了 Collection 接口,它们又分别有自己独有的方法

泛型和集合

可以把泛型看作一个占位符,但是它使用的时候必须传入实际的参数类型

通过使用泛型,编译器会检查放入集合的对象的类型,这样容器使用的时候可以避免类型判断,使用起来更加方便清晰

Collection 接口

AbstractCollection 类提供了 Collection 接口的默认实现,即 AbstractCollection 类是 Collection 接口的实现类

Arrays 类

这个类包含了操作数组的一些方法

java 复制代码
// asList(T...a): 传入一个可变长度的参数列表,会返回一个 List<E> 数组
// E 对应的就是参数的类型
List<Integer> list = Arrays.asList(1, 2, 3, 4);

for (Integer i : list){
    System.out.println(i);
}

// sort(): 对数组进行升序排列
int arr[] = {1,5,6,2,2,3,2,1,4,8};

for (int i : arr){
    System.out.print(i + " ");
}

Arrays.sort(arr);
System.out.println();

for (int i : arr){
    System.out.print(i + " ");
}

// Arrays.toString(): 传入一个数组类型,返回一个字符串
int arr[] = {1,5,6,2,2,3,2,1,4,8};

String s = Arrays.toString(arr);

System.out.println(s);

// Arrays.equals(): 传入两个数组,比较它们是否相等
int arr[] = {1,5,6,2,2,2};
int brr[] = {1,5,6,2,2,3};

boolean bool = Arrays.equals(arr, brr);

System.out.println(bool);

Collections 类

这个类完全由++在 Collection 接口上操作或者返回 Collection 接口++的方法组成

java 复制代码
Collection<Integer> c = new ArrayList<>();

// Collections.addAll(): 传入一个 Collection 接口,和一个可变长参数
// 实现往接口中存入数据的操作
boolean b = Collections.addAll(c, 1, 2, 3, 4, 5);

System.out.println(b);

for (Integer i : c){
    System.out.println(i);
}
7.3、List 接口

List 将元素按照插入的顺序保存,同时它有一些方法,可以在 List 集合的中间插入或移除元素

常用的两个 List 的实现类:

  • ArrayList:随机访问元素的效率高
  • LinkedList:中间插入或移除元素的效率高

ArrayList<E>

LinkedList<E>

7.4、Iterator 迭代器

它可以遍历并选择集合中的对象,而客户端不需要知道底层的操作

它将遍历集合的操作与底层分离,使得不同的集合 (例如List、Set) 都可以通过这个方法遍历集合中的对象

**注意:**Iterator 迭代器只能单向移动

java 复制代码
Collection<String> c = new ArrayList<>();
c.add("abc");
c.add("def");
c.add("xyz");

// c.iterator(): 不传参数
// 返回一个 Iterator<String> 对象
Iterator<String> iterator = c.iterator();

// hasNext(): 不传参数
// 判断序列中是否还有下一个元素
while (iterator.hasNext()){
    
    // next(): 不传参数
    // 获得序列中的下一个元素
    System.out.println(iterator.next());
}

// remove(): 不传参数
// 将集合中最新获得的元素删除,此处删除了 "xyz"
iterator.remove();

IO流

File类:文件和目录的路径名的抽象表示形式

它的实例是不可变的

它只是指向了一个路径,并不是创建一个文件

即使我们指向了一个不存在的路径,代码也不会报错的

java 复制代码
//将指定路径名转化成一个File对象
File f = new File("D:\\a\\b.txt");
//根据指定父路径和文件路径创建File对象
File f = new File("D:\\a","b.txt");
//根据指定的父路径对象和文件路径创建File对象
File f = new File("D:\\a");
File f1 = new File(f1,"b.txt");
java 复制代码
//指定目录下创建文件,这里IO这个文件必须有,否则会报错
//b.txt不存在时会创建并返回true,否则返回false
File f = new File("E:\\IO\\b.txt");
System.out.println(f.createNewFile());
//指定位置创建一个目录
//不能创建多级目录
//A不存在时会创建并返回true,否则返回false
File f1 = new File("E:\\IO\\A");
System.out.println(f1.mkdir());
//指定位置创建多级目录
//这里的d.txt并不是txt文件,是一个目录
File f2 = new File("E:\\IO\\B\\C\\d.txt");
System.out.println(f2.mkdirs());

IO流

流的本质是数据传输,IO流就是用来处理设备间数据传输问题的

按数据流向分为:

输入流:读数据

输出流:写数据

按数据操作类型分为:

字节流:字节输入流(InputStream):FileInputStream:文件输入流

BufferedInputStream:文件缓冲输入流

字节输出流(OutputStream):FileOutputStream:文件输出流

BufferedOutputStream:文件缓冲输出流

字符流::字符输入流(Reader):FileReader:字符输入流

BufferedReader:字符缓冲输入流

InputStreamReader:字符转换流:字节-转换成-字符

字符输出流(Writer):FileWriter:字符输出流

BufferedWriter:字符缓冲输出流

OutputStreamWriter:字符转换流:字符-转换成-字节

java 复制代码
//字节输入输出流是抽象类,不能直接实例化
//FileOutputStream 文件输出流 将数据写入文件
//如果文件不存在,就创建文件
//追加写入功能需要在后面添加 true,这样每运行一次便会写入一次
FileOutputStream fos = new FileOutputStream("E:\\IO\\a.txt",true);
fos.write(100);
fos.close();
    
//FileInputStream 文件输入流 读取数据
FileInputStream fis = new FileInputStream("e:\\io\\a.txt");
//一次读取一个字节
int ch;
while ((ch = fis.read())!=-1){
   System.out.print((char)ch);
}
fis.close();
FileInputStream fis1 = new FileInputStream("e:\\io\\a.txt");
//一次读取一个字节数组
byte[] chs = new byte[1024];
int len;
while((len = fis1.read(chs))!= -1){
    System.out.print(new String(chs,0,len));
}
fis1.close();

//文件字节输入输出流复制图片
FileInputStream fis = new FileInputStream("e:\\io\\123.png");
FileOutputStream fos = new FileOutputStream("e:\\io\\a\\456.png");
//一次读取一个字节数组 推荐
byte[] by = new byte[1024];
int len;
while ((len = fis.read(by))!=-1){
    fos.write(by,0,len);
}
fis.close();
fos.close();


-----------------------------

//就好比FileWriter 和 BufferedWriter 两者的关系一样
//FileOutputStream 和 BufferedOutputStream的关系也是如此
//都是实现数据的高效写入和读取
//字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\io\\a.txt"));
bos.write("世界".getBytes());
bos.write("nihao".getBytes());
bos.close();
//字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\io\\a.txt"));
//两种读数据方式 方式一
//推荐方式二
int ch;
while ((ch = bis.read())!=-1){
    System.out.print((char) ch);
}
//方式二
byte[] bytes = new byte[1024];
int a;
while((a = bis.read(bytes))!=-1){
    System.out.println(new String(bytes,0,a));
}
bis.close();

字符流出现的原因: 当数据中有中文存在时,字节流操作中文不方便

而字节流复制文件时,即使文件中有中文也能复制成功,这是因为有底层进行相关操作

字符输入输出流:

java 复制代码
//使用FileWriter和FileReader
//字符输出流 FileWriter 写数据
//如果目录路径下的文件名不存在就创建一个文件
FileWriter fw = new FileWriter("E:\\IO\\c.txt");
//在文件中写入数据
fw.write("IO");
//这里close()会默认执行flush()方法刷新缓冲区
fw.close();

//字符输入流 FileReader 读数据
//从文件读数据并显示到控制台
//文件路径不存在会报错
FileReader fr = new FileReader("E:\\IO\\b.txt");
//读数据方法一:一次读取一个字符
//通过循环读取,当读取的结果不是 -1 则表示是有数据的
int ch;
while ((ch = fr.read()) != -1){
    //输出语句不要加 ln
    System.out.print((char)ch);
}
fr.close();

//读数据方法二:一次读取一个字符数组
//通过循环读取
char[] ch = new char[1024];
int len;
while((len = fr.read(ch)) != -1){
    System.out.print(new String(ch,0,len));
}
fr.close();

//字符缓冲流
//BufferedWriter 高效写入数据
//BufferedReader 高效读取数据
//使用方法与FileWriter类似,只是创建对象有所不同
BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\IO\\c.txt"));
//但是字符缓冲流读取数据的时候有新的方法
BufferedReader br = new BufferedReader(new FileReader("E:\\IO\\c.txt"));
String line;
//readline() 一次读取一行数据
while((line = br.readLine()) != null){
    //这个加了 ln 起到换行的作用
    System.out.println(line);
}

注意:为了简化书写,读取字符文件时我们可以使用FileWriter和FileReader代替

为了提高效率,提供了字符缓冲流BufferedWriter和BufferedReader

字符转换流:

java 复制代码
//OutputStreamWriter:将写入的字符转换成字节
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("E:\\io\\a.txt"));
osw.write("信号");
osw.flush();
osw.close();

//InputStreamReader:将文件中的字符转换成字节
InputStreamReader isr = new InputStreamReader(new FileInputStream("E:\\io\\a.txt"));
//方式一
int ch;
while ((ch = isr.read())!=-1){
    System.out.print((char)ch);
}
isr.close();
//方式二
char[] chs = new char[1024];
int len;
while((len = isr.read(chs))!= -1){
    System.out.println(new String(chs,0,len));
}
isr.close();
java 复制代码
//复制单级目录下所有文件
    //创建源目录对象
    File oldFile = new File("E:\\IO");
    //获取源目录的名字
    String oldName = oldFile.getName();
    //根据目的目录路径和源目录的目录名创建目的目录对象
    File newFile = new File("E:\\JavaWebDevelopment\\IDEA\\First",oldName);
    //如果目的目录没有这个目录名,就创建
    if (!newFile.exists()){
        newFile.mkdir();
    }
    //遍历源目录所有的文件
    File[] oldList = oldFile.listFiles();
    for (File f : oldList){
        //获取文件名字,并在目的目录下创建文件名
        String oldNames = f.getName();
        File newFiles = new File(newFile,oldNames);
        //复制文件
        //因为有图片,所以使用字节缓冲输入输出流,效率比较高
        copyF(f,newFiles);
    }
}
private static void copyF(File f, File newFiles) throws IOException{
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(newFiles));

    byte[] bytes = new byte[1024];
    int len;
    while ((len=bis.read(bytes))!=-1){
        bos.write(bytes,0,len);
    }
    bis.close();
    bos.close();
}


//复制多级目录下所有文件
    //创建源目录对象
    File oldFile = new File("E:\\IO");
    //创建目的目录对象
    File newFile = new File("E:\\JavaWebDevelopment");
    //执行复制方法
    copyF(oldFile,newFile);
}
private static void copyF(File oldFile, File newFile) throws IOException{
    //如果源目录是一个目录,就在目的目录的基础上创建一个对象指向新的路径
    if (oldFile.isDirectory()){
        //新的路径就是在目的处创建一个和源目录一样的目录
        File newFileName = new File(newFile,oldFile.getName());
        if (!newFileName.exists()){
            newFileName.mkdir();
        }
        //获取目录内所有东西,并且遍历他们,递归调用
        File[] listFiles = oldFile.listFiles();
        for (File f : listFiles){
            copyF(f,newFileName);
        }
        //如果传过来的oldFile是一个文件
    }else {
        //在目的目录的基础上创建一个新的对象指向和源文件同名的路径
        File newF = new File(newFile,oldFile.getName());
        //复制文件内容,传入的参数是源文件和新的文件对象
        copyX(oldFile,newF);
    }
}
private static void copyX(File oldFile, File newF) throws IOException{
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(newF));
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(oldFile));
    byte[] bytes = new byte[1024];
    int len;
    while ((len = bis.read(bytes))!=-1){
        bos.write(bytes,0,len);
    }
    bos.close();
    bis.close();
}




-------------------------------------


// 复制文件异常处理:这样不用释放资源,系统会自动帮我们释放资源
try (FileWriter fw = new FileWriter("e:\\io\\m.txt");
     FileReader fr = new FileReader("e:\\io\\a.txt");){
    char[] ch = new char[1024];
    int len;
    while ((len = fr.read(ch)) != -1) {
        fw.write(ch, 0, len);
    }
}catch (IOException e){
    e.printStackTrace();
}

------------------------------
//标准输入流:InputStream 
InputStream is = System.in;
int by;
while((by = is.read())!=-1){
    System.out.println((char)by);
}
//按照上面的方法当输入中文字符时,使用字节流显然不能很好显示内容
//于是我们提出了字符转换流InputStreamReader(is),把字节流转换成字符流
//但是考虑到多个汉字的情况,还是一行一行读数据比较好
//于是就用到了字符缓冲输入流BufferedReader的readLine方法
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();
System.out.println(s);
//当输入数字的时候,这个方法就会报错了,所以用到了Integer包装类
int i = Integer.parseInt(br.readLine());
//我们可以看出自己写一个方法录入数据比较麻烦,所以Java提供了Scanner类
//标准输出流:PrintStream 
PrintStream ps = System.out;
ps.print(100); 运行后就会在控制台出现100


---------------------------------

打印流:只负责输出数据,不负责读取数据

//字节打印流
PrintStream ps = new PrintStream("e:\\io\\a.txt");
ps.write(97);  使用字节输出流的write方法,会进行转码,结果是a
ps.print(97);  使用自己的print方法,结果是97
ps.close();
//字符打印流
PrintWriter pw = new PrintWriter("e:\\io\\a.txt");
pw.write("hello");  字符流必须刷新或者释放资源才能看到内容
pw.println("hello");  这个也必须刷新或是释放资源才能看内容
//第二种写法,传入字符输出流对象,然后在后面加上true
//这样直接调用方法会自动进行刷新,可以直接看到内容
PrintWriter pw = new PrintWriter(new FileWriter(""e:\\io\\a.txt""),true);
pw.println("hello");

对象序列化流:

序列化:将对象保存在磁盘中或者在网络中传输对象

这种机制就是使用一个字节序列表示一个对象

这个字节序列包括对象的类型,对象的数据和对象中存储的属性等信息

字节序列写到文件之后,就相当于文件中持久保存了一个对象的信息

话不多少,看一下代码

java 复制代码
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\io\\a.txt"));
Student s = new Student("haha",12);
oos.writeObject(s);//控制台会出现部分乱码,使用对象反序列化流读取数据
oos.close();
//使用ObjectInputStream读取ObjectOutputStream写入文件的内容
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\io\\a.txt"));
Object obj = ois.readObject();
Student stu = (Student) obj;
System.out.println(stu.getAge()+stu.getName());
ois.close();

注意:一个对象想要被序列化,就必须实现Serializable接口

这个接口是一个标记接口,所以不用重写什么方法

当我们通过对象序列化流保存了一个文件之后,可以利用反序列化流读取该文件内容

但是:当我们修改了序列化流保存的那个类的内容,那么反序列化流就会读取失败并抛出异常

为什么会读取失败呢,因为序列化的类会声明一个serialVersionID的字段,然后反序列化时候会获得这个字段,之后修改了类内容,它的字段就会发生变化,反序列的字段却还是以前的,所以会报错

建议:可序列化的类显示声明serialVersionID,尽可能使用private修饰,以免引起不必要的错误

java 复制代码
private static final long serialVersionID = 40L;

transient关键字:被这个关键字修饰的成员变量不会被序列化,也就是不会被写入文件

Properties

概述:是一个Map体系的集合类,可以保存到流中或者从流中加载

使用方法和Map集合类似,但是他创建对象的方法就是普通的new,不能加<>泛型

java 复制代码
//创建集合对象
Properties p = new Properties();
//添加数据,和Map添加数据方法一样
p.put("123","一二三");
p.put("456","四五六");
//读取数据,和Map读取数据步骤一样
Set<Object> objects = p.keySet();
for (Object key:objects){
    Object value = p.get(key);
    System.out.println(key+","+value);
}

//Properties特有方法
Properties p = new Properties();
p.setProperty("1a","Hello");
p.setProperty("2b","World");
System.out.println(p);
//根据键 获取 值
System.out.println(p.getProperty("1a"));
//获取一个键的集合,可以在循环里面继续使用getProperty()方法,获取值的内容
Set<String> set = p.stringPropertyNames();
for (String s: set){
    System.out.println(s+","+p.getProperty(s));
}

//Properties和IO流的结合使用
//将集合中的数据写入文件
Properties p = new Properties();
p.setProperty("1a","Hello");
p.setProperty("2b","World");
FileWriter fw = new FileWriter("e:\\io\\a.txt");
p.store(fw,null);
fw.close();
//把文件中的数据加载到集合
FileReader fr = new FileReader("e:\\io\\a.txt");
p.load(fr);
fr.close();
System.out.println(p);

//案例:玩游戏记录次数
//将玩游戏的次数写入文件,每次运行程序时读取文件中游戏进行的次数
//根据次数进行不同操作
Properties p = new Properties();
FileReader fr = new FileReader("e:\\io\\a.txt");
//从文件中获取count和它的次数
p.load(fr);
fr.close();
//从集合中获取次数的数值
String count = p.getProperty("count");
//把count改成int类型
int c = Integer.parseInt(count);
if (c <= 3){
    GameDemo.Guess();
    c++;
    p.setProperty("count",String.valueOf(c));
    FileWriter fw = new FileWriter("e:\\io\\a.txt");
    p.store(fw,null);
    fw.close();

}else {
    System.out.println("已玩三次,请充值...");
}

线程

说到线程,就不得不首先说一下进程,因为线程是依赖于进程的

进程:是正在运行的程序。

系统进行资源分配和调度的独立单位

每个进程都有它自己的内存空间和系统资源

线程:进程中的单个顺序控制流,是一条执行路径

单线程:一个进程如果只有一条执行路径,那么它就是单线程程序

多线程:一个进程如果有多条执行路径,那么它就是多线程程序

了解:线程在执行过程中必须得到CPU资源,由Thread模拟一个虚拟的CPU

我们把CPU执行的代码和操作过程中的数据,传递给Thread类

多线程的实现方式一:Thread

java 复制代码
public class MyThread extends Thread {
    //重写run()方法是用来封装被线程执行的代码
    //因为不一定所有代码都放在线程中,所以需要重写
    @Override
    public void run() {
        for (int i = 0 ; i <100 ; i ++){
            //getName()是获取线程的名称
            System.out.println(getName()+i);
        }
    }
}
//使用
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
//直接使用run()方法并没有启动线程
//mt1.run();
//mt2.run();
//调用start()方法,线程开始执行,Java虚拟机调用此线程的run()方法
//两者区别:我们重写了run()方法,直接调用时,相当于普通方法
//start()方法是由JVM调用线程的run()方法
//mt1.start();
//mt2.start();
//设置线程名称
mt1.setName("AA-:");
mt2.setName("BB-:");
mt1.start();
mt2.start();
//一个类没有继承Thread类,怎么获取它的线程名字
//使用Thread.currentThread()返回当前正在执行的线程对象的引用
 System.out.println(Thread.currentThread().getName());

线程调度

分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占CPU的时间片

抢占调度模型:优先让优先级高的线程使用CPU,优先级高的线程获取的时间片偏多一些

如果优先级一样,则会随机选择

Java是使用抢占调度模型的

java 复制代码
//Thread中设置和获取线程的优先级
//优先级高仅表示获取CPU时间片的几率大
System.out.println("线程最大优先级:"+Thread.MAX_PRIORITY); //10
System.out.println("线程最小优先级:"+Thread.MIN_PRIORITY);  //1
System.out.println("线程默认优先级:"+Thread.NORM_PRIORITY);//5
System.out.println(mt1.getPriority()+","+mt2.getPriority());
mt1.setPriority(3);
mt1.start();
mt2.start();

线程控制

java 复制代码
//线程休眠
//在继承线程的类中使用Thread.sleep(时间);
//线程会在执行的时候进行休眠
//线程等待
//等待当前线程执行完毕后在继续执行之后的线程
mt1.start(); //启动线程一
mt1.join();  //线程一执行完毕之后才会执行下一个线程
mt2.start();
//守护线程
//当运行的线程都是守护线程时,JVM将会退出,也就是程序将会结束

线程的生命周期

一、创建:创建线程对象

二、调用start()方法,线程进入就绪状态:这个状态的线程具有执行资格,但是没有执行权

三、线程抢占CPU执行权

四、抢到了执行权便可以运行:这时候有了执行资格,也有了执行权

运行时如果被其他线程抢夺了CPU执行权,那么线程就会回到就绪状态

运行时如果调用了sleep()方法或者其他阻塞方法,线程就会进入阻塞状态

阻塞状态既没有执行资格,也没有执行权

当sleep()方法或者其他阻塞方法结束,线程才会进入就绪状态

五、run()方法结束或者调用了stop()方法,线程死亡

多线程的实现方式二:Runable接口

java 复制代码
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i =0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"i");
        }
    }
}
//使用
//首先创建MyRunnable对象
//再创建Thread对象,将mr传入
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"AAA");
Thread t2 = new Thread(mr,"BBB");
Thread t3 = new Thread(mr,"CCC");
t1.start();
t2.start();
t3.start();

卖票案例一

java 复制代码
public class MyRunnable implements Runnable {
    //定义100张票
    private int tickets = 100;
    @Override
    public void run() {
        //死循环实现可以一直卖票
        while (true) {
            //如果票数大于0,就输出内容,并进行tickets--;
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + ":第" + tickets + "张票");
                tickets--;
                //卖一张票之后休眠100ms
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                break;
            }
        }
    }
}
//在另一个类中使用
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"窗口一");
Thread t2 = new Thread(mr,"窗口二");
Thread t3 = new Thread(mr,"窗口三");
t1.start();
t2.start();
t3.start();

这样的代码不出现错误,但实际运行后发现会出现一张票卖了多次的情况

这就是多线程的数据安全问题

如何解决?给出一个安全的环境让程序运行

如何实现?把操作共享数据(这里指的是卖火车票的代码)锁起来,让任意时刻都只允许一个线程执行

Java提供了同步代码块来实现这个解决方式

java 复制代码
//同步代码块
Object o = new Object(); 
//()中可以是任意对象
synchronized (o) {
    操作共享数据的代码
    }
//好处:解决了多线程数据安全问题
//坏处:线程很多时,耗费资源,降低效率
//同步方法
public synchronized void method(){代码块}
//synchronized(对象),所以同步方法也应该有锁对象,这里锁对象是this
//静态同步方法
public static synchronized void method(){代码块}

Lock:是一个接口

Lock的实现能 提供比使用synchronized 方法和语句 更广泛的锁定操作

java 复制代码
//Lock的使用
//接口不能实例化,这里采用它的实现类ReentrantLock来进行实例化
private Lock lock = new ReentrantLock();
private int tickets = 100;
@Override
public void run() {
    while (true) {
        //利用lock()方法进行加锁
        //这里使用try的意思是防止加上锁之后里面内容出现错误导致没有办法释放锁
       //而finally语句必然会执行,所以避免了这个错误
       try{
            lock.lock();
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + ":第" + tickets + "张票");
                tickets--;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                break;
            }
        }finally{
        //unlock()方法进行解锁
        lock.unlock();
        }
    }
}

生产者和消费者案例

概述:有一个盒子用来放牛奶

生产者看到里面是空的,就生产并往里面放了牛奶,同时提醒消费者消费(notify()方法唤醒)

生产者看到里面有牛奶,就等待消费者消费(wait()方法等待)

消费者看到里面是空的,就等待生产者生产(wait()方法等待)

消费者看到里面有牛奶,就拿走牛奶,就提醒生产者生产(notify()方法唤醒)

java 复制代码
public class Box {
    //定义一个变量,用来记录牛奶瓶数
    private int milk;
    //定义一个标志,用来记录盒子的状态
    private boolean flag = false;
    public Box(){ 
        
    }
    public Box(int milk) {
        this.milk = milk;
    }
    //生产者生产
    public synchronized void setMilk(int milk) {
        //第一次flag是false,不执行if语句
        //表示盒子是空的,需要添加牛奶
        if(flag) {
            //如果有了牛奶,就等消费者消费
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            this.milk = milk;
            System.out.println("生产者生产了第" + this.milk + "瓶奶");
            //生产之后改变盒子状态,显示有了牛奶了
            flag = true;
            //唤醒消费者线程进行消费
            notifyAll();
        }
    }
    //消费者消费
    public synchronized void getMilk() {
        //如果有牛奶,才能进行消费
        //生产者添加之后flag状态变为true
        if (flag) {
            System.out.println("消费者拿到了第" + this.milk + "瓶奶");
            //拿了牛奶之后,盒子里面就没东西了,所以状态需要改变
            flag = false;
            //唤醒线程
            notifyAll();
        }else {
            //如果没有牛奶,等生产者生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//生产者类
public class Producer implements Runnable{
    private Box box;
    public Producer(Box box) {
        this.box = box;
    }
    @Override
    public void run() {
        //生产者生产,并将数字传入
        for (int i=1;i<10;i++){
            box.setMilk(i);
        }
    }
}
//消费者类
public class Customer implements Runnable {
    private Box box;
    public Customer(Box box) {
        this.box = box;
    }
    @Override
    public void run() {
        while (true){
            //消费者消费
            box.getMilk();
        }
    }
}
//测试类演示生产者消费者案例
public static void main(String[] args) {
    Box box = new Box();
    Producer p = new Producer(box);
    Customer c = new Customer(box);
    Thread t1 = new Thread(p);
    Thread t2 = new Thread(c);
    t1.start();
    t2.start();
}

反射

类加载(类初始化):当程序使用某个类时,如果该类还未被加载到内存中

系统会通过类的加载、类的连接、类的初始化这三个步骤对类进行初始化

如果不出意外,JVM将会连续完成这三个步骤

类的加载:将class文件读入内存,并为之创建一个java.lang.Class类的对象

任何类被使用时,系统都会为其建立一个java.lang.Class类的对象

类的连接:验证阶段:检验被加载的类是否有正确的内部结构,并和其他类协调一致

准备阶段:给类的类变量分配内存,并设置默认初始化值

解析阶段:将类的二进制数据中的符号引用替换为直接引用

类的初始化:主要就是对类的类变量进行初始化

类的初始化步骤:假如该类还未被加载和连接,则程序先加载并连接该类

加入该类的父类还未初始化,则先初始化其父类

类中有初始化语句,系统会依次执行这些初始化语句

注意:第二步初始化父类的时候也是按照这个顺序

类的初始化时机:创建类的实例

调用类的类方法

访问类或者接口的类变量,或者为该类变量赋值

使用反射方法强制创建某个类或者接口对应的java.lang.Class对象

初始化某个类的子类

直接使用java.exe命令来运行某个主类

类加载器

类加载器的作用:负责将.class文件加载到内存中,并为之生成对应的java.lang.Class类的对象

JVM的类加载机制:① 全盘负责:当一个类加载器负责加载某个Class时

该Class所依赖的和引用的其他类也将由该类加载器负责载入

除非显示使用另一个类加载器加载

② 父类委托:当一个类加载器负责加载某个Class时

先让父类加载器试图加载该Class

父类加载器无法加载时,在尝试从自己的类路径中加载该类

③ 缓存机制:所有加载过的Class都会被缓存,当程序使用某个Class对象时,

类加载器首先从缓存区中搜索该Class

缓存区不存在该Class对象时,系统才会读取该类的二进制数据,

将其转换为Class对象,存入缓存区

java 复制代码
//获取类加载器对象
//返回用于委派的系统类加载器c
ClassLoader c = ClassLoader.getSystemClassLoader();
System.out.println(c);
//返回c的父类加载器
ClassLoader c1 = c.getParent();
System.out.println(c1);
//返回c1的父类加载器
ClassLoader c2 = c1.getParent();
System.out.println(c2);

Java反射机制:运行时去获取一个类的变量和方法信息

然后通过获取到的信息去创建对象、调用方法的一种机制

着啊样子体现了很好的动态性,让程序更加灵活

java 复制代码
反射获取Class对象
//想要通过反射使用一个类
//首先需要获取该类的字节码文件(.class)对象,也就是类型为Class的对象
//获取Class类的对象
//使用类的class属性来获取该类对应的Class对象
Class<Student> studentClass = Student.class;
System.out.println(studentClass);
//调用对象的getClass()方法,返回对象所属类对应的Class对象
Student student = new Student();
Class<? extends Student> aClass = student.getClass();
System.out.println(aClass);
//使用Class类中的静态方法forName(String className)方法
//className是某个类的包路径
Class<?> forNameClass = Class.forName("one.Student");
System.out.println(forNameClass);

反射获取构造方法
//首先获取Student类的Class类的对象
//也就是获取字节码文件对象
Class<?> c = Class.forName("one.Student");

//获取构造方法对象
//此方法只能得到public修饰的构造方法的对象
Constructor<?>[] cons1 = c.getConstructors();
for (Constructor cons: cons1) {
    System.out.println(cons);
}

//此方法可以得到所有构造方法对象
Constructor<?>[] cons2 = c.getDeclaredConstructors();
for (Constructor cons: cons2) {
    System.out.println(cons);
}

//获取单个以public修饰的无参构造方法对象
Constructor<?> con = c.getConstructor();
//通过Constructor<T>中的newInstance()方法创建对象
//newInstance()根据指定的构造方法创建对象
Object obj = con.newInstance();
System.out.println(obj);

//获取单个以public修饰的带参构造方法对象
Constructor<?> con1 = c.getConstructor(String.class,int.class);
//通过Constructor<T>中的newInstance()方法创建对象
//newInstance()根据指定的构造方法创建对象
Object obj1 = con1.newInstance("早某人",20);
System.out.println(obj1);

//获取private修饰的带参构造方法对象
//获取构造方法对象
//通过getDeclaredConstructor()方法
Constructor<?> privateConstructor = c.getDeclaredConstructor(String.class);
/*
* //创建对象并使用
* Object obj = privateConstructor.newInstance("赵某人");
* System.out.println(obj);
* 但这样子写会报错,因为不能使用私有的构造方法创建对象
* */
//在反射当中,却可以使用私有的构造方法创建对象
//暴力反射setAccessible(boolean flag),当传入参数为true,可以取消访问检查

privateConstructor.setAccessible(true);

Object obj = privateConstructor.newInstance("赵某人");
System.out.println(obj);

反射获取成员变量
//获取Class类的对象
//也就是获取字节码文件对象
Class<?> c = Class.forName("one.Student");

//getFields()方法会返回一个Field对象的数组
//Field对象反映了由该Class对象表示的类的所有可访问的公共成员对象
//也就是说这个数组里的东西都是  代表公共成员变量的对象
Field[] f1 = c.getFields();
for (Field f: f1){
   System.out.println(f);
}
//getDeclaredFields()方法会返回一个Field对象的数组
//Field对象反映了由该Class对象表示的类的所有的成员对象
//也就是说这个数组里的东西都是  代表成员变量的对象
Field[] f2 = c.getDeclaredFields();
for (Field f: f2){
     System.out.println(f);
}
        //怎么通过反射使用单个的成员变量
        //以前是这样子操作的
//        Student s = new Student();
//        s.address = "运城";
        //反射中是这样操作的
        //创建一个无参的构造方法对象
        Constructor<?> declaredConstructor = c.getDeclaredConstructor();
        //通过构造方法对象创建对象
        //这里o就相当于Student s里的s
        Object o = declaredConstructor.newInstance();
        //获取指定成员变量对应的对象
        Field addressField = c.getField("address");
        //给成员变量赋值
        //set(Object obj,Object value)方法是给obj对象的成员变量对象(ageField)赋值
        addressField.set(o,"山西");
        System.out.println(o);
        
反射获取成员方法
Class<?> c = Class.forName("one.Student");
//返回本类以及本类继承的类的公共方法
Method[] m1 = c.getMethods();
//返回本类中的所有方法
Method[] m2 = c.getDeclaredMethods();
for (Method m : m2) {
    System.out.println(m);
}
//返回一个公共方法并使用
//传入方法名和对应的参数类,例如("method",String.class)
Method method1 = c.getMethod("method1", null);
Constructor<?> con1 = c.getConstructor();
Object o = con1.newInstance();
//方法调用,
//这个方法的意思是 o 调用 method1()方法
//跟我们平时的正好相反
method1.invoke(o);
//返回一个私有方法并使用
Method method2 = c.getDeclaredMethod("method2", null);
method2.setAccessible(true);
method2.invoke(o); 

反射越过泛型检测

java 复制代码
ArrayList<Integer> arr = new ArrayList<>();
//此时arr集合中只能添加Integer元素,否则编译时会报错
//运用反射我们可以越过检测
//获取集合的Class对象
Class<? extends ArrayList> c = arr.getClass();
//返回方法对象
//传入add()方法的方法名add和参数类型,我们把Integer.class改成Objct.class
Method add = c.getMethod("add", Object.class);
//arr调用add,传入"hello"字符串
add.invoke(arr,"hello");
System.out.println(arr);

反射之配置文件

java 复制代码
//加载数据
Properties p = new Properties();
FileReader fr = new FileReader("E:\\JavaWebDevelopment\\IDEA\\First\\src\\one\\class.txt");
p.load(fr);
fr.close();
//获取类名和方法名
String className = p.getProperty("className");
String mName = p.getProperty("mName");
//获取类对象
Class<?> aClass = Class.forName(className);
//根据构造方法对象使用newInstance()创建对象
Constructor<?> constructor = aClass.getConstructor();
Object o = constructor.newInstance();
//返回方法对象
//传入方法名
Method method = aClass.getMethod(mName);
//方法调用
method.invoke(o);

Stream流

作用:真正的把函数式编程风格引入到了Java中

步骤:生成流 -> 中间操作 -> 终结操作

生成流:通过数据源(集合、数组等)生成流

中间操作:一个流后面可以跟随一个或者多个中间操作

其目的是打开流,作出某种程度的数据过滤,然后返回一个新的流,交给下一个操作使用

终结操作:一个流只能有一个终结操作,这个操作执行之后,流无法再被操作

生成流:

java 复制代码
//Stream流的常见生成方式
//Collections体系的集合可以通过默认方法stream()生成流
List<String> listStream = new ArrayList<>();
listStream.stream();
//Map体系的集合间接生成流
Map<String,Integer> map = new HashMap<>();
//根据(key)键生成键的流
Stream<String> keyStream = map.keySet().stream();
//根据(value)值生成值的流
Stream<Integer> valueStream = map.values().stream();
//根据(entrySet)键值对生成键值对的流
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
//数组可以通过Stream接口的静态方法of(T...values)生成流
String[] str = {"a","b","c"};
//可以将数组名传入生成流
Stream<String> str1Stream = Stream.of(str);
//也可以直接将数组元素传入生成流
Stream<String> str2Stream = Stream.of("a", "b", "c");

中间操作:

java 复制代码
//这里创建一个集合来使用流
ArrayList<String> arr = new ArrayList<>();
arr.add("赵某人");
arr.add("张某人");
arr.add("张三");
arr.add("赵四");
arr.add("哈哈哈哈哈");
//filter的使用
//filter(Predicate p):对流中的信息过滤
//Predicate接口中的test()方法可以对给定参数进行判断
//生成流并使用filter进行中间操作,运用foreach进行终结操作
arr.stream().filter(s->s.startsWith("赵")).forEach(System.out::println);
arr.stream().filter(s -> s.length()>2).forEach(System.out::println);
arr.stream().filter(s -> s.length()>2)
            .filter(s -> s.startsWith("赵"))
            .forEach(System.out::println);
            
//limit 和 skip的使用
//limit(i)方法是截取前i个数据
//skip(i)方法是跳过前i个数据
arr.stream().limit(3).forEach(System.out::println);
arr.stream().skip(3).forEach(System.out::println);
arr.stream().skip(2).limit(2).forEach(System.out::println);
//concat(Stream a,Stream b) 和 distinct的使用
//concat(Stream a,Stream b)方法是合并两个流
//distinct()方法是去掉重复的元素
Stream<String> streamA = arr.stream().limit(2);
Stream<String> streamB = arr.stream().skip(1);
Stream.concat(streamA,streamB).forEach(System.out::println);
Stream.concat(streamA,streamB).distinct().forEach(System.out::println);

//sorted() 和 sorted(Comparator c)的使用
//sorted()方法是按照首字母顺序进行排序
arr.stream().sorted().forEach(System.out::println);
//sorted(Comparator c)方法是按照我们给定的比较器进行排序
//这里给定的是按照数据的长度进行比较
arr.stream().sorted((s1,s2)->s1.length()-s2.length()).forEach(System.out::println);
//第二种方法中,字符串长度相同时,会默认按照添加顺序进行排序
//为了让字符串在长度相同时仍按照a~z的顺序进行排序,我们给出优化
//首先在后面继续使用sorted()方法,结果发现会把我们给定比较器的排序方法给覆盖掉
//于是只能在Lambda表达式中做修改
arr.stream().sorted((s1,s2)->{
    int num = s1.length() - s2.length();
    //当长度相同时,我们比较两个字符串内容
    //compareTo()返回1,表示ASCII值 s1-s2>0,即字母表中s1比s2靠后
    int num2 = num==0?s1.compareTo(s2):num;
    return num2;
}).forEach(System.out::println);

//map(Function f) 和 mapToInt(ToIntFunction tif)方法
//map(Function f)方法是流和函数式接口Function<T,R>的结合使用
arr.stream().map(s -> Integer.parseInt(s)).forEach(System.out::println);
//mapToInt()方法会返回一个IntStream,因此可以使用IntStream中的方法
int sum = arr.stream().mapToInt(Integer::parseInt).sum();
System.out.println(sum);

终结操作:

java 复制代码
//foreach(Consumer c) 和 count()的使用
//forEach()方法是对流的每个元素执行操作
arr.stream().forEach(System.out::println);
//count()方法是最后返回此流中的元素数,结果是一个long类型
long l = arr.stream().count();
System.out.println(l);

收集流:

java 复制代码
List<String> arr = new ArrayList<>();
arr.add("赵某人");
arr.add("张某人");
arr.add("张吃撑");
arr.add("招收自");
arr.add("哈哈");

Stream<String> stringStream = arr.stream().filter(s -> s.length() > 2);
//将流中的数据保存到list集合当中,使用collect(Collector c)方法
//但是这个参数Collector是一个接口,不能直接使用
//于是使用Collectors类中的toList()方法
stringStream.collect(Collectors.toList()).forEach(System.out::println);

Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);

Stream<Integer> setStream = set.stream().filter(a -> a > 25);
//使用Collectors类中的toSet()方法
//将数据保存在set集合当中
setStream.collect(Collectors.toSet()).forEach(System.out::println);

//将字符串中的数据存入Map集合,姓名为键,年龄为值
String[] str = {"哈哈,20","呵呵,60","嘻嘻,80"};
Stream<String> strStream = Stream.of(str).filter(s -> Integer.parseInt(s.split(",")[1]) > 25);
Map<String, Integer> map = strStream.collect(Collectors.toMap(s -> s.split(",")[0], s -> Integer.parseInt(s.split(",")[1])));
Set<String> keySet = map.keySet();
for (String key :keySet){
    Integer value = map.get(key);
    System.out.println(key+","+value);
}

网络编程

InetAddress:此类表示IP地址

方法使用

java 复制代码
//因为InetAddress没有构造方法DESKTOP-V5RBFCU
//所以我们采用InetAddress.getByName()方法得到InetAddress的一个对象
//方法参数可以是计算机名称或者IP地址,推荐IP地址
InetAddress address = InetAddress.getByName("192.168.1.109");
//得到该address对象的主机名
System.out.println(address.getHostName());
//得到该address对象的IP地址
System.out.println(address.getHostAddress());

端口:设备上应用程序的唯一标识

端口号:两个字节表示的整数,范围在0~65535

0~1023之间的端口号用于一些知名的服务和应用

普通程序需要使用1024以上的端口号

如果一个端口号被另一个服务或者应用占用,会导致当前程序启动失败

协议:计算机网络中,连接和通信的规则叫网络通信协议

UDP:UDP协议是一种不可靠的网络协议,它在通信的两端各建立了一个Socket

但这两个Socket只是发送、接受数据的对象

因此,对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念

使用:Java提供了DatagramSocket类作为基于UDP协议的Socket

我们通过这个类实现UDP协议中发送和接受数据

java 复制代码
//UDP发送数据
//1.创建发送数据的Socket对象
DatagramSocket socket = new DatagramSocket();
//2.创建数据,并把数据打包
//构造一个数据包,该数据包需要发送长度为length的数据包到指定主机的指定端口号
byte[] bytes = "Hello,udp,我来了".getBytes();
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,
        InetAddress.getByName("192.168.1.103"),10086);
//3.调用方法发送数据包
socket.send(dp);
//4.关闭发送端
socket.close();

//UDP接收数据
//1.创建接收数据的Socket对象,并且需要指定端口
DatagramSocket datagramSocket = new DatagramSocket(10086);
//2.创建一个数据包用于接收数据
//构造一个用于接收长度为length的数据包
byte[] bytes1 = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(bytes1,bytes1.length);
//3.调用方法接收数据
datagramSocket.receive(datagramPacket);
//4.解析数据包,即从数据包中拿到数据
byte[] data = datagramPacket.getData();
//通过getLength()方法得到数据的长度
int len = datagramPacket.getLength();
//将字节数组的数据转换成字符串
String s = new String(data,0,len);
System.out.println(s);
//5.关闭接收端
datagramSocket.close();
//运行的时候,首先运行接收端,发现没有数据
//           紧接着运行发送端,接收端便受到了数据

//UDP通信练习,实现接收端一直接受发送端的数据
//可以打开多个发送端,可以实现仿制聊天室功能
//发送端
//创建发送端对象
DatagramSocket ds = new DatagramSocket();
//从键盘录入数据,采用Scanner或者BufferedReader
BufferedReader br = new BufferedReader(new InputStreanReader(System.in));
String line;
while((line = br.readline())!=null){
    //当数据不是886,就一直输入
    if(line.equals("886")){
        break;
    }else{
        //将键盘获取的数据存入字符数组
        byte[] b = line.getBytes();
        //构造一个数据包,****处写的是ip地址
        DatagramPacket dp = new DatagramPacket(b,b.length
                            ,InetAddress.getByName("****"),10086);
       //发送数据 
       ds.send(dp);
    }
}
//关闭发送端
ds.close();

//接收端
//创建接收端对象,并指定端口号
DatagramSocket ds = new DatagramSocket(10086);
//死循环,一直获取发送端发送的内容
while(true){
    //创建一个字符数组用来存储数据
    byte[] b = new byte[1024];
    //构造一个数据包用来接收数据
    DatagramPacket dp = new DatagramPacket(b,b.length());
    //调用接收方法接受数据
    ds.receive(dp);
    //将接收到的数据传入字符数组
    byte[] data = dp.getData();
    //将字符数组转成String字符串输出到控制台
    System.out.println(new String(data,0,dp.getLength()));
}

TCP:一种可靠的网络通信协议

在通信的两端各建立一个Socket对象,形成网络虚拟链路

一旦建立了虚拟的网络链路,两段程序就可以通过虚拟链路进行通信

使用:Java对基于TCP的网络提供了良好的封装

使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信

Java微客户端提供了Socket类,为服务端提供了ServerSocket类

java 复制代码
//使用TCP发送数据
//创建客户端的Socket对象,指定主机和端口
Socket socket = new Socket("192.168.1.103",10086);
//创建输出流,写数据
OutputStream os = socket.getOutputStream();
os.write("hello我来了".getBytes());
//释放资源
socket.close();
os.close();

//TCP接收数据
//创建服务器端对象
ServerSocket serverSocket = new ServerSocket(10086);
//监听客户端的连接,返回对应的Socket对象
Socket s = serverSocket.accept();
//获取输入流,读取数据
InputStream is = s.getInputStream();
//读取数据
byte[] b = new byte[1024];
int len;
while((len = is.read(b))!= -1){
    System.out.println(new String(b,0,len));
}
s.close();
serverSocket.close();

//TCP通信练习,实现服务端一直接受客户端的数据
//TCP接收数据
//创建服务器端对象
ServerSocket serverSocket = new ServerSocket(10086);
//监听客户端的连接,返回对应的Socket对象
Socket s = serverSocket.accept();
//获取输入流,读取数据
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
String line;
while ((line = br.readLine())!=null){
    System.out.println(line);
}
serverSocket.close();
//TCP发送数据
//创建客户端的Socket对象,指定主机和端口
Socket socket = new Socket("192.168.1.103",10086);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String line;
while ((line = br.readLine())!=null){
    if (line.equals("886")) {
        break;
    }else {
        bw.write(line);
        bw.newLine();
        bw.flush();
    }
}
//释放资源
socket.close();

//TCP客户端从文件中读取数据并发送到服务器
//服务器给出文件上传成功的提示
//客户端
//创建客户端的Socket对象,指定主机和端口
Socket s = new Socket("192.168.1.103",10086);
//从文件中读取数据
BufferedReader br = new BufferedReader(new FileReader("e:\\io\\a.txt"));
//写数据,获取输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line;
while((line = br.readLine())!=null){
        bw.write(line);
        bw.newLine();
        bw.flush();
}
//输出结束标记
s.showdownOutput();
//读取数据,接收服务器端的反馈
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String s  = bufferedReader.readLine();
System.out.println(s);
//释放资源
br.close();
s.close();

//服务端
//创建AerverSocket对象接收数据,传入端口号
ServerSocket serverSocket = new ServerSocket(10086);
//监听客户端的连接,返回对应的Socket对象
Socket s = serverSocket.accept();
//读取数据,获取输入流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\IO\\.txt"));
String line;
while ((line = br.readLine())!=null){
    bw.write(line);
    bw.newLine();
    System.out.println(line);
    bw.flush();
}
//写数据,将内容给客户端进行反馈
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bufferedWriter.write("文件ok");
bufferedWriter.newLine();
bufferedWriter.flush();
bw.close();
serverSocket.close();
java 复制代码
// 导出 word
// 这里接受的是一个 <String,String> 的 map
// 前台需要 JSON.stringify(xxx).toString(); 把数据转化成 String 类型 
// 后台通过 JSONArray arr = JSONArray.parseArray(map.get("budgets"),xxx.class); 获取对象数组
@PostMapping(value = "/outF")
public Result<?> download(@RequestBody HashMap<String,String> map,HttpServletRequest request,
    							       HttpServletResponse response){
       try {
          String fileName = "D:/temp/导出模板.docx";
          File file = new File(fileName);
          FileInputStream in = new FileInputStream(file);
          XWPFDocument doc = new XWPFDocument(in);
    
          //给指定表格内容新增行数据
          List<XWPFTable> tables = doc.getTables();// 获取word中所有的表格

          for (int i = 0; i < tables.size(); i++) {
             XWPFTable table = tables.get(i);//获取第一个表格
             // 模拟添加行数据
             // j < info.size()
             for (int j = 0; j < 3; j++) {
                XWPFTableRow newRow = insertRow(table, 0, j + 1);
                // 在列中插入数据
                List<XWPFTableCell> cellList = newRow.getTableCells();
                for (XWPFTableCell cell : cellList) {
                   // cell.setText(list.get(i).get(j));
                   cell.setText("表格内容_" + j);
                }
             }
          }

          //替换指定位置的文本:
          // 使用文本段落处理
          List<XWPFParagraph> paragraphList = doc.getParagraphs();
          Map<String, Object> param = new HashMap<String, Object>();
          param.put("${code}", map.get("code"));
          param.put("${xmName}", map.get("xmName"));
          param.put("${fzrName}", map.get("fzrName"));
          param.put("${charger}", map.get("charger"));
          param.put("${phone}", map.get("phone"));
          param.put("${email}", map.get("email"));
          param.put("${writeDate}", map.get("createTime"));
          param.put("${year}", map.get("createTime").substring(0,4));
          param.put("${month}", map.get("createTime").substring(5,7));
          param.put("${lxnd}", map.get("lxnd"));
          Calendar date = Calendar.getInstance();
    
          param.put("${yearFooter}", String.valueOf(date.get(Calendar.YEAR)));
          param.put("${monthFooter}", String.valueOf(date.get(Calendar.MONTH)+1));
          param.put("${dayFooter}", String.valueOf(date.get(Calendar.DAY_OF_MONTH)));
          processParagraphs(paragraphList, param, doc);

          FileOutputStream out = new FileOutputStream(new File("D:/temp/Word导出结果.docx"));
          doc.write(out);
          out.flush();
          out.close();
    
       } catch (IOException e) {
          e.printStackTrace();
       }
    
           return Result.ok();
    }

 /**
  *
  * 处理段落中文本,替换文本中定义的变量;
  *
  * 变量定义格式${xxx}
  *
  * @param paragraphList
  *
  *            段落列表
  *
  * @param param
  *
  *            需要替换的变量及变量值
  *
  * @param doc
  *
  *            需要替换的DOC
  */
 public static void processParagraphs(List<XWPFParagraph> paragraphList,Map<String, Object> param, XWPFDocument doc) {
    if (paragraphList != null && paragraphList.size() > 0) {


           /*
           for (XWPFParagraph paragraph : paragraphList) {
               List<XWPFRun> runs = paragraph.getRuns();
               for (XWPFRun run : runs) {
                   String text = run.getText(0);
                   if (text != null) {
                       boolean isSetText = false;
                       for (Entry<String, Object> entry : param.entrySet()) {
                           String key = entry.getKey();
                           if (text.indexOf(key) != -1) {
                               isSetText = true;
                               Object value = entry.getValue();
                               if (value instanceof String) {// 文本替换
                                   text = text.replace(key, value.toString());
                               }
                           }
                       }
                       if (isSetText) {
                           run.setText(text, 0);
                       }
                   }
               }
           }*/

       // 替换段落中的指定文字
       Iterator<XWPFParagraph> itPara = doc.getParagraphsIterator();//获得word中段落
       while (itPara.hasNext()) {       //遍历段落
          XWPFParagraph paragraph = (XWPFParagraph) itPara.next();
          Set<String> set = param.keySet();
          Iterator<String> iterator = set.iterator();
          while (iterator.hasNext()) {
             String key = iterator.next();
             List<XWPFRun> run=paragraph.getRuns();
             for(int i=0;i<run.size();i++)
             {
                if(run.get(i).getText(run.get(i).getTextPosition())!=null &&
                      run.get(i).getText(run.get(i).getTextPosition()).equals(key))
                {
                   /**
                    * 参数0表示生成的文字是要从哪一个地方开始放置,设置文字从位置0开始
                    * 就可以把原变量替换掉
                    */
                   run.get(i).setText((String)param.get(key),0);
                }
             }
          }
       }
    }
 }

 /**
  * 插入表格行
  * insertRow 在word表格中指定位置插入一行,并将某一行的样式复制到新增行
  * @param copyrowIndex 需要复制的行位置
  * @param newrowIndex 需要新增一行的位置
  * */
 public static XWPFTableRow insertRow(XWPFTable table, int copyrowIndex, int newrowIndex) {
    // 在表格中指定的位置新增一行
    XWPFTableRow targetRow = table.insertNewTableRow(newrowIndex);
    // 获取需要复制行对象
    XWPFTableRow copyRow = table.getRow(copyrowIndex);
    //复制行对象
    targetRow.getCtRow().setTrPr(copyRow.getCtRow().getTrPr());
    //或许需要复制的行的列
    List copyCells = copyRow.getTableCells();
    //复制列对象
    XWPFTableCell targetCell = null;
    for (int i = 0; i < copyCells.size(); i++) {
       XWPFTableCell copyCell = (XWPFTableCell) copyCells.get(i);
       targetCell = targetRow.addNewTableCell();
       targetCell.getCTTc().setTcPr(copyCell.getCTTc().getTcPr());
       if (copyCell.getParagraphs() != null && copyCell.getParagraphs().size() > 0) {
          targetCell.getParagraphs().get(0).getCTP().setPPr(copyCell.getParagraphs().get(0).getCTP().getPPr());
          if (copyCell.getParagraphs().get(0).getRuns() != null
                && copyCell.getParagraphs().get(0).getRuns().size() > 0) {
             XWPFRun cellR = targetCell.getParagraphs().get(0).createRun();
             cellR.setBold(copyCell.getParagraphs().get(0).getRuns().get(0).isBold());
          }
       }
    }

    return targetRow;

 }


// 导出 excel
/**
 * 导出excel
 *
 * @param request
 * @param response
 * @return
 * @throws Exception
 */
@CrossOrigin
@PostMapping("/exportExcel")
public void exportExcel(@RequestBody HashMap<String,String> map, HttpServletRequest request, HttpServletResponse response) {
 try {


    // 模板文件
    XSSFWorkbook wb = new XSSFWorkbook(new FileInputStream("D:/temp/Excel导出模板.xlsx"));

    // 根据页面index 获取sheet页
    XSSFSheet sheet = wb.getSheetAt(0);

    //下面目前写死的数据表示从页面传入的项目基本信息
    XSSFCell cellYear = sheet.getRow(2).getCell(0);
    cellYear.setCellValue("(2020年度)");

    XSSFCell cellName = sheet.getRow(4).getCell(3);
    cellName.setCellValue("测试项目1");

    XSSFCell cellDept = sheet.getRow(5).getCell(3);
    cellDept.setCellValue("技术部");

    XSSFCell cellXTDept = sheet.getRow(5).getCell(7);
    cellXTDept.setCellValue("人事部");

    XSSFCell cellYXXM = sheet.getRow(6).getCell(3);
    int[] code = {0x2610, 0x2611};
    String tempStr1 = new String(code,0,1) + "延续项目(已有项目的升级、改版等)";
    tempStr1 += new String(code,1,1) + "新增项目";
    cellYXXM.setCellType(XSSFCell.CELL_TYPE_STRING);
    cellYXXM.setCellValue(tempStr1);

    XSSFCell cellXMZQ = sheet.getRow(6).getCell(7);
    cellXMZQ.setCellValue("2020-12-01前");

    XSSFCell cellZJE = sheet.getRow(7).getCell(3);
    cellZJE.setCellValue("1000(万元)");

    XSSFCell cellNDMB = sheet.getRow(10).getCell(1);
    cellNDMB.setCellValue("目标1:完成A计划\r\n目标2:完成B计划\r\n目标3:完成计划\r\n");


    //以下为模拟页面输入的指标信息
    //模拟一、二、三级指标数据结构

    //构建三级指标数据(每个Map模拟一个JavaBean)
    Map<String,String> map3Data1 = new HashMap<String,String>();
    map3Data1.put("三级指标1", "三级指标Value_1");

    Map<String,String> map3Data2 = new HashMap<String,String>();
    map3Data2.put("三级指标2", "三级指标Value_2");

    Map<String,String> map3Data3 = new HashMap<String,String>();
    map3Data3.put("三级指标3", "三级指标Value_3");

    List<Map> list3_1 = new ArrayList<Map>();//对应某一二级指标的三级指标集合1
    list3_1.add(map3Data1);
    list3_1.add(map3Data2);
    list3_1.add(map3Data3);

    Map<String,String> map3Data4 = new HashMap<String,String>();
    map3Data4.put("三级指标4", "三级指标Value_4");

    Map<String,String> map3Data5 = new HashMap<String,String>();
    map3Data5.put("三级指标5", "三级指标Value_5");

    List<Map> list3_2 = new ArrayList<Map>();//对应某一二级指标的三级指标集合2
    list3_2.add(map3Data4);
    list3_2.add(map3Data5);


    Map<String,String> map3Data6 = new HashMap<String,String>();
    map3Data6.put("三级指标6", "三级指标Value_6");

    Map<String,String> map3Data7 = new HashMap<String,String>();
    map3Data7.put("三级指标7", "三级指标Value_7");

    List<Map> list3_3 = new ArrayList<Map>();//对应某一二级指标的三级指标集合3
    list3_3.add(map3Data6);
    list3_3.add(map3Data7);

    Map<String,String> map3Data8 = new HashMap<String,String>();
    map3Data8.put("三级指标8", "三级指标Value_8");

    Map<String,String> map3Data9 = new HashMap<String,String>();
    map3Data9.put("三级指标9", "三级指标Value_9");

    Map<String,String> map3Data10 = new HashMap<String,String>();
    map3Data10.put("三级指标10", "三级指标Value_10");

    List<Map> list3_4 = new ArrayList<Map>();//对应某一二级指标的三级指标集合4
    list3_4.add(map3Data8);
    list3_4.add(map3Data9);
    list3_4.add(map3Data10);


    //构建二级指标数据1
    Map<String,List> map2 = new HashMap<String,List>();
    map2.put("二级指标1", list3_1);//指定二级指标对应的三级指标集合
    map2.put("二级指标2", list3_2);

    //构建二级指标数据2
    Map<String,List> map22 = new HashMap<String,List>();
    map22.put("二级指标3", list3_3);//指定二级指标对应的三级指标集合
    map22.put("二级指标4", list3_4);

    //构建一级指标数据
    Map<String,Map> map1 = new HashMap<String,Map>();
    map1.put("产出指标", map2);
    map1.put("效益指标", map22);


    //单元格样式

    //字体颜色设置
    XSSFFont fontColor1 = wb.createFont();
    fontColor1.setColor(IndexedColors.GREEN.getIndex());
    fontColor1.setFontName("宋体"); // 字体
    fontColor1.setFontHeightInPoints((short) 12); // 大小

    XSSFFont fontColor2 = wb.createFont();
    fontColor2.setColor(IndexedColors.BLACK.getIndex());
    fontColor2.setFontName("宋体"); // 字体
    fontColor2.setFontHeightInPoints((short) 12); // 大小

    //单元格样式1
    XSSFCellStyle setBorderGreen = wb.createCellStyle();
    //边框设置
    setBorderGreen.setBorderBottom(XSSFCellStyle.BORDER_THIN); //下边框
    setBorderGreen.setBorderLeft(XSSFCellStyle.BORDER_THIN);//左边框
    setBorderGreen.setBorderTop(XSSFCellStyle.BORDER_THIN);//上边框
    setBorderGreen.setBorderRight(XSSFCellStyle.BORDER_THIN);//右边框
    //字体颜色设置
    setBorderGreen.setFont(fontColor1);
    //居中设置
    setBorderGreen.setAlignment(XSSFCellStyle.ALIGN_CENTER);
    setBorderGreen.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER);

    //单元格样式2
    XSSFCellStyle setBorderBlack = wb.createCellStyle();
    //边框设置
    setBorderBlack.setBorderBottom(XSSFCellStyle.BORDER_THIN); //下边框
    setBorderBlack.setBorderLeft(XSSFCellStyle.BORDER_THIN);//左边框
    setBorderBlack.setBorderTop(XSSFCellStyle.BORDER_THIN);//上边框
    setBorderBlack.setBorderRight(XSSFCellStyle.BORDER_THIN);//右边框
    //字体颜色设置
    setBorderBlack.setFont(fontColor2);
    //居中设置
    setBorderBlack.setAlignment(XSSFCellStyle.ALIGN_CENTER);
    setBorderBlack.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER);

    setBorderBlack.setWrapText(true);

    //单元格样式3
    XSSFCellStyle setBorderGreen2 = wb.createCellStyle();
    //边框设置
    setBorderGreen2.setBorderBottom(XSSFCellStyle.BORDER_THIN); //下边框
    setBorderGreen2.setBorderLeft(XSSFCellStyle.BORDER_THIN);//左边框
    setBorderGreen2.setBorderTop(XSSFCellStyle.BORDER_THIN);//上边框
    setBorderGreen2.setBorderRight(XSSFCellStyle.BORDER_THIN);//右边框
    //字体颜色设置
    setBorderGreen2.setFont(fontColor1);
    //居中设置
    setBorderGreen2.setAlignment(XSSFCellStyle.ALIGN_CENTER);
    setBorderGreen2.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER);
    //设置文字竖向显示
    setBorderGreen2.setRotation((short)255);

    //循环遍历组装的数据
    int rowIndex1 = 14;//初始行,行数以三级指标数为准
    int rowIndex2 = 14;//初始行,行数以三级指标数为准
    int rowIndex3 = 14;//初始行,行数以三级指标数为准

    Iterator<String> it1 = map1.keySet().iterator();
    while(it1.hasNext()){//遍历一级指标
       String key = it1.next();

       //创建rowIndex行
       XSSFRow row1 = sheet.createRow(rowIndex1);

       XSSFCell cell1ZBName1 = row1.createCell(1); //一级指标1
       cell1ZBName1.setCellValue(key);

       cell1ZBName1.setCellStyle(setBorderGreen2);

       Map mp2 = map1.get(key);//获取二级指标
       Iterator<String> it2 = mp2.keySet().iterator();
       while(it2.hasNext()){//遍历二级指标
          String key2 = it2.next();
          XSSFRow row2 = null;
          if(rowIndex1 == rowIndex2){
             row2 = row1;
          }else{
             row2 = sheet.createRow(rowIndex2);

             row2.createCell(0).setCellStyle(setBorderGreen);
             row2.createCell(1).setCellStyle(setBorderGreen);
          }

          XSSFCell cell2ZBName1 = row2.createCell(2); //二级指标1
          cell2ZBName1.setCellValue(key2);

          cell2ZBName1.setCellStyle(setBorderGreen);

          List<Map> list3 = (List)mp2.get(key2);//获取某二级指标下的三级指标集合
          for(Map mp3 : list3){
             Iterator<String> it3 = mp3.keySet().iterator();
             if(it3.hasNext()){
                String key3 = it3.next();
                XSSFRow row3 = null;
                if(rowIndex2 == rowIndex3){
                   row3 = row2;
                }else{
                   row3 = sheet.createRow(rowIndex3);

                   row3.createCell(0).setCellStyle(setBorderGreen);
                   row3.createCell(1).setCellStyle(setBorderGreen);
                   row3.createCell(2).setCellStyle(setBorderGreen);
                }

                row3.setHeight((short)(24*20));

                XSSFCell cell3ZBName1 = row3.createCell(3); //设置三级指标名
                cell3ZBName1.setCellValue(key3);
                cell3ZBName1.setCellStyle(setBorderBlack);

                XSSFCell cell3ZBValue1 = row3.createCell(4); //设置三级指标值
                cell3ZBValue1.setCellValue((String)mp3.get(key3));
                cell3ZBValue1.setCellStyle(setBorderBlack);


                //合并EFGHI五个单元格
                CellRangeAddress region3 = new CellRangeAddress(rowIndex3, rowIndex3, 4, 8);
                sheet.addMergedRegion(region3);

                // 使用RegionUtil类为合并后的单元格添加边框
                RegionUtil.setBorderBottom(1, region3, sheet, sheet.getWorkbook()); // 下边框
                RegionUtil.setBorderLeft(1, region3, sheet, sheet.getWorkbook()); // 左边框
                RegionUtil.setBorderRight(1, region3, sheet, sheet.getWorkbook()); // 有边框
                RegionUtil.setBorderTop(1, region3, sheet, sheet.getWorkbook()); // 上边框

                rowIndex3++;//下一行
             }
          }

          //竖向合并二级指标单元格
          CellRangeAddress region2 = new CellRangeAddress(rowIndex2, rowIndex3-1, 2, 2);
          sheet.addMergedRegion(region2);
          rowIndex2 = rowIndex3;
       }
       
       //竖向合并一级指标单元格
       CellRangeAddress region1 = new CellRangeAddress(rowIndex1, rowIndex2-1, 1, 1);
       sheet.addMergedRegion(region1);
       rowIndex1 = rowIndex2;
    }
    
    //竖向合并绩效指标单元格
    CellRangeAddress region1 = new CellRangeAddress(13, rowIndex1-1, 0, 0);
    sheet.addMergedRegion(region1);

    // 输出Excel文件
    OutputStream output = new FileOutputStream(new File("D:/temp/新的.xlsx"));
    response.reset();
    // 设置文件头
    response.setHeader("Content-Disposition",
          "attchement;filename=" + new String("人员信息.xlsx".getBytes("gb2312"), "ISO8859-1"));
    response.setContentType("application/msexcel");
    wb.write(output);
    output.close();
 } catch (Exception e) {
    e.printStackTrace();
 }
}

反射

1、反射机制

Java 通过反射机制可以在程序运行期间获取类的信息,并能操作任何一个对象的内部属性和方法

总之就是使得 java 具有了一定的动态性

2、获取 Class 对象

程序运行时,会将类的 class 文件读入内存,并为该类创建一个 java.lang.Class 类的对象

通过反射获取一个类的 Class 对象有三种方式。

  • 方式一:通过 Class.forName() 方法

    java 复制代码
    public class Hello {
        public static void main(String[] args) throws ClassNotFoundException {
    
            // 获取不同类的 Class 对象
            Class<?> c1 = Class.forName("com.ck.World");
            Class<?> c2 = Class.forName("java.lang.String");
    
            System.out.println(c1);
            System.out.println(c2);
        }
    }
  • 通过 object.getClass() 方法

    java 复制代码
    public class Hello {
        public static void main(String[] args) throws ClassNotFoundException {
    
            String str = "hello";
            World w = new World();
    
            System.out.println(str.getClass());
            System.out.println(w.getClass());
        }
    }
  • 通过 类名.class 获得

    java 复制代码
    public class Hello {
        public static void main(String[] args) throws ClassNotFoundException {
    
            System.out.println(String.class);
            System.out.println(World.class);
        }
    }

任何一个类都只有一个 Class 对象,一个类创建的时候只会加载一次

java 复制代码
public class Hello {
    public static void main(String[] args) throws ClassNotFoundException {

        // 通过三种方法分别获得 com.ck.World 类的 Class 对象
        Class c1 = Class.forName("com.ck.World");

        Class c2 = new World().getClass();

        Class c3 = World.class;

        // 比较它们的内存地址
        System.out.println(c1 == c2);  // true
        System.out.println(c2 == c3);  // true
    }
}
3、类的加载过程

程序使用某个类的时候,如果该类还没有被加载到内存中,系统会通过类的加载、类的连接、类的初始化这三个步骤完成类加载

  • 类的加载:类加载器将类的 class 文件读入内存,并创建一个 Class 对象

    • 程序使用任何类的时候,系统都会为之建立一个 Class 对象
    • 类加载器:后面详细讲
  • 类的连接:将类的二进制数据合并入 JRE 当中

    • 验证:验证被加载的类是否有正确的内部结构,判断是否符合 java 编写规范
    • 准备:为类的类变量(static)分配内存地址,并设置默认初始值
    • 解析:将二进制文件中的符号引用(变量名)替换为直接引用(内存地址)的过程
  • 类的初始化:JVM 对类进行初始化,主要是对类变量(static)赋予正确的初始值

    • 声明类变量指定初始值

    • 使用静态代码块赋初始值

      java 复制代码
      public class Hello{
          public static int a = 100;
          public static int b;
          static{
              b = 3;
          }
      }

    **注意:**初始化一个类的时候,如果其父类没有进行初始化,则会先初始化其父类

    类初始化的时机:

    • 创建类的实例,也就是 new 对象的时候
    • 调用某个类的静态变量,或者对某个类的静态变量赋值的时候
      • 这里需要注意 final ,调用 final 修饰的静态变量编译的时候就确定了的话,不会初始化类
    • 调用类的静态方法的时候
    • 反射获取类的 Class 对象的时候
    • 初始化某个类的子类,其夫类也会被初始化
    • 直接使用 java.exe 运行某个类,运行时,程序会首先初始化该类
4、通过反射创建对象
4.1 newInstance() 方法

在反射机制中,通过 Class 对象的 newInstance() 方法,可以调用指定类的无参构造方法,创建其对象

java 复制代码
public class Hello {
    public static void main(String[] args) throws ClassNotFoundException,IllegalAccessException, InstantiationException {
  
        // 通过反射创建对象
        Class<?> c = Class.forName("com.ck.World");
        Object o = c.newInstance();
        System.out.println(o);
                                                  
        // 直接创建对象
        World w = new World();
        System.out.println(w);
    }
}

public class World {
    public World(){
        System.out.println("调用了无参构造方法");
    }
}
4.2 两种方式的优点

通过反射创建对象和直接创建对象各有好处:

  • 直接创建对象,代码简洁易懂
  • 通过反射创建,可以在不改变代码的情况下创建不同的对象
    • Properties 的 load() 方法可以传入一个 Reader 或者一个 InputStream,故而有两种写法加载配置文件
    • 可以通过相对路径获取文件的绝对路径
java 复制代码
public class Hello {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        
        // 直接创建对象
        World w = new World();
        // 创建不同类的对象时需要在现有代码上进行修改,此处即添加下面一行代码
        Earth e = new Earth();
        
        // 使用反射和配置文件联合创建对象

        // 写法一:返回一个 Reader
        // String path = Thread.currentThread().getContextClassLoader().getResource("two.properties").getPath();
        // FileReader fr = new FileReader(path);

        // 写法二:返回一个 InputStream 流
        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("two.properties");
        
        // 创建属性类对象
        Properties prop = new Properties();
        // 加载配置文件
        prop.load(is);

        // 记得关闭流
        is.close();

        // 通过反射创建对象时候,通过读取配置文件的 className 属性来获取要创建的对象所在的类
        // 这里创建不同的对象,只需要修改配置文件 one.properties,而不需要修改现有代码
        String className = prop.getProperty("className");
        System.out.println(className);
    }
}

public class Earth {}

public class World{}
properties 复制代码
# className=com.ck.Earth
className=com.ck.World
4.3 资源绑定器

Java 提供了一个资源绑定器,不过只能用于读取类路径下 (src) 的以 properties 结尾的属性配置文件

java 复制代码
public class Hello {
    public static void main(String[] args) {
        // 直接获取配置文件
        // src 目录下直接通过配置文件名即可
        ResourceBundle b1 = ResourceBundle.getBundle("two");
        // src 的子包下通过配置文件路径即可
        ResourceBundle b2 = ResourceBundle.getBundle("com/ck/one");

        // 获取属性值
        String s1 = b1.getString("className");
        String s2 = b2.getString("className");
        
        System.out.println(s1);
        System.out.println(s2);
    }
}
5、类加载器(了解)

类加载器:ClassLoader

Java 中的类加载器负责将 .class 文件加载到内存上,将其放在方法区内,并为之生成对应的 Class 对象

一个类只会加载一次,当一个类被载入到 JVM 中,同一个类就不会再次载入了,那么怎么判断是否是同一个类?

Java 中,一个类的全限定名(包名 + 类名)作为一个类的标识

Java 中自带了三个类加载器:

  • 根加载器:Bootstrap ClassLoader(祖宗)

  • 扩展类加载器:Extension ClassLoader(爸爸)

  • 当前应用类加载器:System ClassLoader(儿子)

  • 自定义类加载器

注意:访问根加载器或者 Extension ClassLoader 的父加载器,会返回 null

类加载机制:

  • 全盘负责:一个类加载器负责加载某个 Class 时,该 Class 依赖和引用的其他 Class 也由该类加载器加载
  • 父类委托:首先会让父类加载器加载某个 Class,父类加载器无法加载时,再尝试用自己的类加载器
  • 缓存机制:所有被加载过的 Class 都会被缓存,程序使用时,先从缓存区寻找,找不到时才会通过类加载器工作

双亲委派机制:为了保证类加载的安全

当一个类加载器收到类加载任务,首先会从 Bootstrap ClassLoader 和 Extension ClassLoader 中进行加载

如果这俩加载器都没法加载,再从当前应用类加载器中加载

6、反射获取类的信息
6.1 反射获取类的属性
java 复制代码
public class Hello {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("com.ck.World");

        // 获取公开(public)的属性的信息
        Field[] fields = c.getFields();
        for (Field field : fields){
            String fieldName = field.getName();
            String fieldTypeName = field.getType().getSimpleName();
            String fieldType = Modifier.toString(field.getModifiers());
            System.out.println(fieldType + "--" + fieldTypeName + "--" + fieldName);
        }

        System.out.println("=====");

        // 获取所有的属性的信息
        Field[] declaredFields = c.getDeclaredFields();
        for (Field field : declaredFields){
            String fieldName = field.getName();
            String fieldTypeName = field.getType().getSimpleName();
            String fieldType = Modifier.toString(field.getModifiers());
            System.out.println(fieldType + "--" + fieldTypeName + "--" + fieldName);
        }

        // 给公开(public)的属性赋值/读取属性值
        // 通过 Class 对象的 newInstance() 方法创建一个 World 对象
        Object o = c.newInstance();

        // 获取名字为 num 的属性
        Field num = c.getField("num");

        // 给属性赋值
        num.set(o,123);

        // 获取属性值
        System.out.println(num.get(o));

        // 给私有属性赋值/获取私有属性的值
        Object obj = c.newInstance();
        Field sex = c.getDeclaredField("sex");

        // 暴力破解,设置私有属性的值的时候,开启这个之后才有效果
        sex.setAccessible(true);

        sex.set(obj,false);
        System.out.println(sex.get(obj));

    }
}
6.2 反射获取类的方法
java 复制代码
public class Hello {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("com.ck.World");

        // 获取所有的方法信息
        Method[] methods = c.getDeclaredMethods();
        for (Method method : methods){
            // 方法名
            String methodName = method.getName();
            // 返回值类型
            String methodTypeName = method.getReturnType().getSimpleName();
            // 方法修饰符
            String methodType = Modifier.toString(method.getModifiers());

            // 返回方法的参数
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (Class parameter : parameterTypes){
                System.out.println(parameter.getSimpleName());
            }
            System.out.println(methodType + "--" + methodTypeName + "--" + methodName);
            
        // 通过反射调用方法
        // 首先创建对象
        Object o = c.newInstance();
            
        // 调用哪一个方法?,方法名,参数列表对应的 Class 对象
        Method a = c.getDeclaredMethod("a", String.class, String.class);
            
        // methodName.invoke(object,args...)
        // invoke() 方法表示:object 调用 methodName 方法,传入参数为 args 的列表
        Object invoke = a.invoke(o, "123", "456");
        System.out.println(invoke);
        }
    }
6.3 反射获取构造方法
java 复制代码
public class Hello {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("com.ck.World");

        // 获取所有的构造器对象
        Constructor<?>[] constructors = c.getDeclaredConstructors();
        
        for (Constructor con : constructors){
            
            // 这里的方法名其实也就是类名
            String name = c.getSimpleName();
            String type = Modifier.toString(con.getModifiers());
            System.out.println(type + "--" + name);
            
            // 获取参数列表的 Class 对象
            Class[] parameterTypes = con.getParameterTypes();
            for (Class param : parameterTypes){
                System.out.println(param.getSimpleName());
            }
        }
        
        // 通过构造方法创建对象
        // 无参
        // Class 对象调用 newInstance() 方法,是默认调用无参构造
        // Object o1 = c.newInstance();
        // System.out.println(o1);
        
        // 通过构造函数调用无参构造方法,不需要传入参数的信息
        Constructor<?> con1 = c.getConstructor();
        Object o2 = con1.newInstance();
        System.out.println(o2);

        // 通过构造函数调用有参构造方法,需要传入参数对应的信息
        Constructor<?> declaredConstructor = c.getDeclaredConstructor(int.class, String.class);
        Object o3 = declaredConstructor.newInstance(1, "hello");
        System.out.println(o3);
    }
}
6.4 反射获取父类和接口
java 复制代码
public class Hello {
    public static void main(String[] args) throws Exception {
        // 这里对 String 类进行测试
        // 首先获取 String 类的 Class 对象
        Class<?> str = Class.forName("java.lang.String");

        // 获取其父类的 Class 对象
        Class<?> superClass = str.getSuperclass();
        System.out.println(superClass.getSimpleName());

        // 获取其实现的接口的 Class 对象
        Class<?>[] interfaces = str.getInterfaces();
        for (Class c : interfaces){
            System.out.println(c.getSimpleName());
        }
    }
}

IO 流

IO 流分类:

  • 按照读取数据的方式分类:
    • 字节流,可以读取声音、文本、图片等大部分内容
    • 字符流,主要用来解决文本中的中文字符乱码问题
  • 按照流的方向分类:
    • 输入、读数据
    • 输出、写数据

四个祖宗:

  • 字节输入流:InputStream

  • 字节输出流:OutputStream

  • 字符输入流:Reader

  • 字符输出流:Writer

常用的 IO 流:

  • 文件常用
    • FileInputStream
    • FileOutputStream
    • FileReader
    • FileWriter
  • 缓冲流
    • BufferedInputStream
    • BufferedOutputStream
    • BufferedReader
    • BufferedWriter
  • 转换流:字节流转换成字符流
    • InputStreamReader
    • OutputStreamWriter
  • 标准输出流
    • PrintStream
    • PrintWriter
  • 对象序列化流
    • ObjectInputStream
    • ObjectOutputStream

文件拷贝思路

java 复制代码
public static void main(String[] args) throws IOException {

        File src = new File("D:\\Typora\\TyporaData\\语言");
        File target = new File("E:\\");
        copyMyFile(src,target);

    }

    private static void copyMyFile(File src, File target) {
        
        // 这是递归的结束条件
        if (src.isFile()){
            FileInputStream fis = null;
            FileOutputStream fos = null;

            try {
                fis = new FileInputStream(src);
                String path = (target.getAbsolutePath().endsWith("\\") ?
                        target.getAbsolutePath() : target.getAbsolutePath() + "\\")
                        + src.getAbsolutePath().substring(21);
                fos = new FileOutputStream(path);

                int len;
                byte[] bytes = new byte[1024];
                while ((len = fis.read(bytes)) != -1) {
                    fos.write(bytes,0,len);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return;
        }
        
        // 下面相当于是拷贝目录
        // 用到了递归
        File[] files = src.listFiles();
        for (File f : files){
            if (f.isDirectory()){
                String end = (target.getAbsolutePath().endsWith("\\") ?
                        target.getAbsolutePath() : target.getAbsolutePath() + "\\")
                        + f.getAbsolutePath().substring(21);
                File file = new File(end);
                if (!file.exists()){
                    file.mkdirs();
                }
            }
            copyMyFile(f,target);
        }
    }

序列化和反序列化

序列化:将 java 对象转化成字节流的过程

反序列化: 将字节流恢复成 java 对象的过程

当 java 对象需要持久化到文件中或者在网络上进行传输的时候,就需要对 java 对象进行序列化处理

实现序列化和反序列化的 java 对象必须实现 Serializable 接口

  • 这个接口是一个标志接口,不包含任何方法
  • java 虚拟机看到某个类实现了这个接口,会自动给这个类生成一个序列化版本 ID
  • 这个序列化版本 ID 决定了你是否可以成功反序列化
  • 只有当序列化的 ID 和反序列化的 ID 一样时候,才可以反序列化

就像你之前写过一段代码,进行了序列化,但是没有自己声明序列化 ID,虚拟机帮你自动生成了这个,ID,之后你对一些数据对象进行了序列化,后来你有改动了代码,那么之前序列化的内容都无法反序列化了,因为你不知道序列化的 ID ,就没办法反序列化

为了避免这个问题,编写类实现 Serializable 接口的时候,就手动指定一个序列化 ID

可以对多个对象进行序列化,但是要把对象存入集合当中

如果对象有多个属性,可以通过 transient 关键字指定不需要序列化的属性

线程

1、线程和进程

进程:处于运行过程中的程序,它有一定的独立功能,操作系统中所有运行的任务通常对应一个进程。

线程:线程是进程的一个执行单元,一个进程中可以有多个线程,它们可以并发执行,同时它们可以共享进程中的资源。

进程的特点:

  • 独立性:进程在系统中是独立存在的,每一个进程都有自己独有的内存地址空间。
  • 动态性:进程是运行过程中的程序,但它不是静态的代码,它是抽象的一个概念,进程有不同的生命周期和不同的状态。
  • 并发性:多个进程可以在单个处理器上并发执行,且它们之间不会互相影响。

并发和并行

并发:同一时刻,只能有一条指令在处理器上执行,但是多个进程在快速轮换执行(动态性),所以会有多个进程同时执行的错觉。

并行:同一时刻,有多条指令在多个处理器上同时执行。

2、线程的创建方式
2.1 继承 Thread 类

通过继承 Thread 类实现线程创建的步骤:

  • 定义一个类继承 Thread 类,重写其中的 run() 方法,run() 方法的方法体是该线程需要完成的任务
  • 创建类的实例
  • 调用 start() 方法启动线程
java 复制代码
// 1.继承 Thread 类
public class Hello extends Thread {

    // 2.重写 run() 方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
           
            // this.getName():获取当前正在执行的线程名
            System.out.println(this.getName() + ":" + i);
        }
    }

    // main() 方法
    public static void main(String[] args) {
        // 3.创建类的实例
        Hello hello = new Hello();
        
        // 4.调用 start() 方法
        hello.start();

        for (int i = 0; i < 100; i++){
            // Thread.currentThread():返回当前正在执行的线程对象
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}


// 运行结果:
// 主线程和子线程交替执行循环中的语句

**调用 start() 方法的作用:**启动子线程,在当前进程中开辟一个新的栈空间,开辟完成之后 start() 方法结束

调用 start() 方法和调用 run() 方法的区别:

  • 调用 start() 方法会启动线程,线程自动调用 run() 方法,且 run() 方法中的代码和 main() 方法中的代码会并发执行。
  • 调用 run() 方法,则相当于是调用普通方法,只有 run() 方法执行完毕之后,main() 方法中接下来的代码才可以执行。

调用 start() 方法启动的线程会自动调用 run() 方法,run() 方法在分支栈的栈底,它就相当于主栈中的 main() 方法,都在栈的底部

2.2、实现 Runnable 接口

通过实现 Runnable 接口实现线程创建的步骤:

  • 定义一个类实现 Runnable 接口,重写接口中的 run() 方法
  • 创建类的实例
  • 创建 Thread 类的实例,并将我们定义的类的实例作为参数传入
  • 调用 start() 方法启动线程
java 复制代码
// 1.实现 Runnable 接口
public class Hello implements Runnable {
    
    // 2.重写 run() 方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        // 3.创建 Runnable 实现类的实例
        Hello h = new Hello();

        // 4.创建 Thread 类的实例,并将实现类的实例作为参数传入
        Thread t = new Thread(h);
        
        // 5.调用 start() 方法启动线程
        t.start();

        for (int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

使用实现 Runnable 接口的方式创建线程,多个线程可以共享线程类的实例变量

2.3、实现 Callable<T> 接口

通过 Callable<T> 接口实现线程创建的步骤:

  • 定义一个类实现 Callable 接口,重写其 call() 方法
  • 通过实现类创建 Callable 对象
  • 将 Callable 对象封装为 FutureTask 对象 (FutureTask 间接实现了 Runnable 接口)
  • 创建 Thread 类的实例
  • 通过 start() 方法启动线程
java 复制代码
// 1.实现 Callable<T> 接口
public class Hello implements Callable<Integer> {

    // 2.重写 call() 方法
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + i);
        }

        return i;
    }

    public static void main(String[] args) {
        // 3.创建 Callable 对象
        Hello h = new Hello();
        
        // 4.创建 FutureTask 对象,需要传入一个 Callable 对象的参数
        FutureTask<Integer> futureTask = new FutureTask<>(h);

        // 5.创建 Thread 对象,将 FutureTask 对象作为参数传入
        Thread t = new Thread(futureTask);
        
        // 6.调用 start() 方法启动线程
        t.start();

        for (int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + i);
        }
        
        // 7.获取子线程的返回值,需要捕获异常
        //   获取返回值的时候,会阻塞当前线程(这里就是 main 线程)
        try {
            System.out.println("子线程返回值为:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

注意:我觉得第三个方法特别麻烦,因为它封装了两次。

Callable 接口:

  • 有泛型限制

  • 重写的方法为 call() 方法,该方法有返回值,返回值与泛型类型相同

  • 通过 FutureTask 类将 Callable 对象封装为一个 Runnable 的子类对象

  • 可以通过 get() 方法获取子线程的返回值,但是必须等到子线程结束后才会得到返回值

3、线程的生命周期

线程被创建后,有一定的生命周期,并不是直接创建,然后运行、删除的顺序

3.1 新建状态

当在程序中通过 new 关键字创建一个 Thread 对象之后,线程就处于新建状态

处于新建状态的线程对象与其它 Java 对象一致,都是由 JVM 对其分配内存空间并进行初始化

3.2 就绪状态

当线程对象调用 start() 方法之后,线程就进入了就绪状态,此时,该线程具有抢夺 CPU 时间片(执行权)的权利

进入就绪状态的线程想要运行,还需要获得 CPU 的执行权

3.3 运行状态

线程处于就绪状态,并且抢到了 CPU 的执行权时,该线程开始调用 run() 方法执行,此时线程进入运行状态

当抢占的 CPU 时间片用完(执行权结束)后,该线程会重新进入就绪状态,等待下一次抢到了 CPU 执行权再继续执行

3.4 阻塞状态

运行状态的线程遇到了阻塞事件,该线程会放弃占有的 CPU 时间片(执行权),进入阻塞状态

进入阻塞状态的线程等待事件结束后,会重新进入就绪状态进行 CPU 执行权的抢夺

阻塞事件:

  • 线程睡眠:调用 sleep() 方法,会让线程在指定时间内进入阻塞状态,时间结束后线程进入就绪状态

  • 线程等待:调用 wait() 方法,会让当前的线程等待,直到其他线程调用此对象的 notify() 方法进行唤醒后进入就绪状态

  • 线程合并:调用 join() 方法,在一个线程中调用另一个线程的 join() 方法,使当前线程进入阻塞状态

    ​ 直到另一个线程结束后,该线程才进入就绪状态

3.5 死亡状态

当 run() 方法执行结束,该线程进入死亡状态

4、线程的常用方法
4.1 获取线程对象

静态方法:Thread.currentThread().getName()

通过 Thread.currentThread() 可以获取当前线程对象,即 Thread t = Thread.currentThread();

通过 setName()、getName() 可以对线程的名字进行设置、获取

java 复制代码
public class Hello implements Runnable {
    
    @Override
    public void run() {
        System.out.println("子线程: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Hello h = new Hello();

        Thread t = new Thread(h);
        t.setName("child");
        t.start();

        System.out.println("主线程名字: " + Thread.currentThread().getName());
        System.out.println("子线程名字: " + t.getName());
    }
}
4.2 线程休眠

静态方法:sleep()

让当前线程进入休眠状态,此时线程会陷入阻塞状态,舍弃自己占有的执行权 (时间片)

java 复制代码
public class Hello implements Runnable {

    @Override
    public void run() {
       for (int i = 0; i < 5; i++){
           System.out.println(i);
           
           // 调用 sleep() 方法需要捕获或者抛出异常
           try { 
               // 子线程休眠 1s
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }

    public static void main(String[] args) throws InterruptedException {
        Hello h = new Hello();
        Thread t = new Thread(h);
        t.start();

        for (int i = 10; i < 15; i++){
            System.out.println(i);
            
            // 主线程休眠 2s
            Thread.sleep(2000);
        }
    }
}

需要注意的是:sleep() 方法是 Thread 类的静态方法,与调用者无关。

​ 如果使用线程对象调用sleep() 方法,会被转换成 Thread.sleep(); 并让当前线程 (主线程) 休眠相应时间

java 复制代码
public class Test extends Thread{
    
    @Override
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println("child");
        }
    }
    
    public static void main(String[] args){
        Test t = new Test();
        t.start();
        
        try{
            // 这样写,程序运行时会被转化成 Thread.sleep(5000);
            // 此处表示的含义:让主线程休眠 5s
            t.sleep(5000); // Thread.sleep(5000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

合理的中止一个线程:可以通过设置一个布尔值来进行标记

java 复制代码
public class Hello extends Thread {
    public boolean flag = true;
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            if (flag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + i);
            } else {
                return;
            }
        }
    }
}

// 主类
public class World{
    public static void main(String[] args) throws InterruptedException {
        Hello h = new Hello();
        h.start();

        System.out.println("2s 后,中止子线程...");
        Thread.sleep(2000);
        h.flag = false;
    }
}
4.3 中断线程

interrupt()

interrupt() 方法可以中断一个线程,但是网上查到的说法是给线程设置一个中断标志,但是线程还会继续运行

与它相似的两个方法:

  • 静态方法:interrupted():判断当前线程是否已经被中断,其实就是检查中断标志,执行此方法后会清除线程的中断状态
  • isInterrupted():判断线程是否被中断,仅仅是判断,没有其他含义
java 复制代码
public class Hello extends Thread {

    @Override
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println("child");
        }
    }

    public static void main(String[] args) {
        Hello h = new Hello();
        h.start();

        System.out.println("子线程是否被中断?" + h.isInterrupted()); // false
        Thread.currentThread().interrupt(); // 给主线程添加中断标志位,但它并不中断主线程
        
        // 这里需要注意了!这个 interrupted() 方法是静态方法,判断的是当前线程是否已经被中断
        // 因此,这里的实例对象调用最终会变成类调用,即:Thread.interrupted(),故而此处表示判断主线程是否被中断
        System.out.println(h.interrupted()); // true,运行后会清除中断标志位
        System.out.println(h.interrupted()); // false
    }
}
4.4 线程合并

join()

在一个线程中调用其他线程的 join() 方法时,该线程将被阻塞,直到另一个线程执行完毕后再继续执行

java 复制代码
public class Hello extends Thread {

    @Override
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println(Thread.currentThread().getName() + i);
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Hello a = new Hello();
        Hello b = new Hello();

        a.setName("A");
        b.setName("B");

        a.start();
        b.start();

        // 此处使用 join() 方法,表示当 b 线程执行完毕之后,主线程才开始执行
        // 注意:这里是在主线程里调用 b 线程的 join() 方法,与 a 线程执行顺序无关
        b.join();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
4.5 线程调度

Java 采用的是抢占式调度模型:讲的是哪个线程的优先级更高,那么它抢到 CPU 之后的执行时间会多一些

Java 可以对获取并修改线程的优先级

setPriority() 和 getPriority()

java 复制代码
public class Hello extends Thread {
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getPriority());
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }

    public static void main(String[] args) {
        System.out.println("线程最高优先级" + Thread.MAX_PRIORITY);  // 10
        System.out.println("线程最低优先级" + Thread.MIN_PRIORITY);  // 1
        System.out.println("线程默认优先级" + Thread.NORM_PRIORITY);  // 5

        Hello a = new Hello();
        Hello b = new Hello();

        a.setName("A");
        b.setName("B");

        // 对两个线程的优先级进行设置
        a.setPriority(7);
        b.setPriority(3);

        a.start();
        b.start();

        System.out.println("主:" + Thread.currentThread().getPriority());  // 5
    }
}
4.6 线程礼让

静态方法:yield()

调用 yield() 方法,会暂停当前正在执行的线程对象,将执行机会让给其他线程 (优先同级别或者更高的线程)

注意,线程礼让时,线程不会阻塞,而是直接进入就绪状态,简单的就可以当作该线程的执行时间用完了

java 复制代码
public class Hello implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            // 当 i % 100 == 0 时,礼让一次
            if (i % 100 == 0){
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + i);
        }
    }

    public static void main(String[] args) {

        // Hello h = new Hello();
        // Thread t = new Thread(h);
        
        // 这里用了简便(合并)写法
        Thread t = new Thread(new Hello());
        t.setName("T");

        t.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("Main 线程" + i);
        }
    }
}
5、线程安全

多线程并发运行时,可能会出现线程安全问题,那怎么才会出现线程安全呢?

  • 处于多线程并发的环境
  • 有共享数据
  • 有对共享数据进行修改的行为

以上三个条件出现之后,可能会发生线程安全问题。

局部变量不会共享,所以是线程安全的

实例变量和静态变量是有可能共享的,线程不安全

5.1 取款案例

用取款编写一个例子演示一下线程不安全

这里是创建了一个账户,开启两个线程模拟两个人,对同一账户进行取款操作

java 复制代码
// Account 账户类
public class Account {

    private int num;
    private int money;

    // 无参构造、有参构造、get、set 方法太长了就不贴在这里了

    // 取钱方法
    public void takeMoney(int takeMoney){
        // 现在的余额
        int now = this.getMoney();
        
        // 取钱
        now = now - takeMoney;

        // 模拟网络延迟
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 更新余额
        this.setMoney(now);
    }
}

// 账户线程类
public class AccountThread implements Runnable{

    // 这里的 Account 账户是多个线程共享的数据对象
    private Account account;

    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        // 取钱
        account.takeMoney(300);
        System.out.println(account.getNum() + "的余额为:" + account.getMoney());
    }
}

// 测试类
public class Hello {
    public static void main(String[] args) {
        // 创建账户
        Account account = new Account(12345,1000);
        
        // 创建账户线程类
        AccountThread a = new AccountThread(account);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(a);

        t1.start();
        t2.start();
    }
}

/* 运行结果:
   12345的余额为:700
   12345的余额为:700
*/
5.2 同步代码块

对于上面的例子,为了让其变成线程安全的,可以使用 synchronized 同步代码块,这里利用了线程同步机制

解决线程安全问题的一般方案:

  • 方案一:尽量使用++局部变量++ 代替++成员变量++ 和++静态变量++

  • 方案二:如果必须使用实例变量,可以考虑创建多个对象,不同线程使用不同对象,使得实例变量的不被共享

  • 方案三:如果上面都不行,那就用 synchronized,线程同步机制

    • 使用线程同步机制,会让效率变差

这里着重介绍了方案三的用法,主要是为了了解 synchronized 关键字

synchronized () { ... }

Java 中,每个对象都默认会有一把锁 (只是一个标记的称呼)

当线程处于运行状态时遇到了 synchronized 关键字

  • 首先会释放之前占有的 CPU 时间片,同时进入锁池 (lockpool) 寻找对象锁
  • 找到对象锁之后,占有它并进入就绪状态继续抢夺 CPU 的执行权
  • 没找到就在锁池中等待

由此可见,synchronized 同步代码块的关键之处就在于 () 中的对象锁

  • 这个锁一定要是进入这个方法的多个线程共享的对象

  • 要是这个锁选择错了,线程依旧是不安全的

java 复制代码
// 取钱方法
public void takeMoney(int takeMoney){
    // 添加 synchronized 关键字
    // () 中是多个线程共享的数据
    synchronized (this) {
        int now = this.getMoney();
        now = now - takeMoney;
        
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        this.setMoney(now);
    }
}

**注意:**这里的 synchronized (this) 的作用

假设有多个线程 t1、t2、t3,都进行取钱的操作,但是 t1、t2 从同一个账户(a1)取钱,t3 是从另一个账户(a2)取钱

那么这里的 this 在 t1、t2、t3 中表示的含义是不一样的

  • 对于 t1、t2,this 是对它们共享的账户对象 a1
  • 对于 t3,this 是 a2

但是如果写成 synchronized ("hello") ,"hello" 字符串在常量池中只有一个,因此它也有一个自己的锁,它被所有线程共享

于是这样写的结果就是 t1、t2、t3 进行取钱时都需要等待上一个线程执行结束

synchronized 关键字在普通方法上,等价于且只等价于方法体中的 synchronized (this) 这样的写法

java 复制代码
// 取钱方法
// 当需要锁住的共享对象是 this,可以将 synchronized 关键字写在方法体上
public synchronized void takeMoney(int takeMoney){
    int now = this.getMoney();
    now = now - takeMoney;

    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    this.setMoney(now);
}

synchronized 关键字在静态方法上,表示的是类锁,即将整个类锁住了

当多个线程访问同一个类的不同对象的时候,会按顺序依次执行,因为类锁,只有当这个类被释放了其他线程才能使用

java 复制代码
public class Account{
    public synchronized static void doSome(){
        // 代码块
    }
    
    public synchronized static void doOther(){
        // 代码块
    }
}

public class AccountThread extends Thread{
    private Account account;

    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            account.doSome();
         }
        if (Thread.currentThread().getName().equals("t2")){
            account.doOther();
        }
    }
}

public class Hello{
    public static void main(String[] args) throws InterruptedException {
        Account a1 = new Account();
        Account a2 = new Account();
        
        AccountThread at1 = new AccountThread(a1);
        AccountThread at2 = new AccountThread(a2);

        Thread t1 = new Thread(at1);
        Thread t2 = new Thread(a);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

// 只有当 t1 执行完毕释放了类,t2 才能开始执行

再次注意:只有线程遇到了 synchronized 关键字才会进行锁机制

如果有多个线程,其中有些线程没有遇到 synchronized 关键字,那么它们会并发执行

5.3 死锁

多个线程,互相锁住了需要访问的方法,导致程序死了

java 复制代码
public class One extends Thread{
    Object o1;
    Object o2;

    public One(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        // 外层把 o1 锁住,只有执行完了才会释放 o1
        synchronized (o1){
            
            // 为了确保发生死锁
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 内层代码是又有了一个 synchronized ,把 o2 锁住,只有执行完了里面的代码才会释放 o2
            synchronized (o2){
                System.out.println("hello");
            }
        }
    }
}

public class Two implements Runnable{
    Object o1;
    Object o2;

    public Two(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        // 外层把 o2 锁住,只有执行完了才会释放 o2
        synchronized (o2){
            
            // 为了确保发生死锁
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 内层代码是把 o1 锁住,只有执行完了里面的代码才会释放 o1
            synchronized (o1){
                 System.out.println("hello");
            }
        }
    }
}

public class Hello {
    public static void main(String[] args) throws InterruptedException {
       Object o1 = new Object();
       Object o2 = new Object();

        // 两个线程共享 o1,o2
       One t1 = new One(o1,o2);
       Thread t2 = new Thread(new Two(o1,o2));

        // 开启线程后,就会发生死锁
        // 因为 t1 要访问 o2 时,发现 o2 被 t2 锁住了,然后它就等待 t2 释放 
        // 同时 t2 要访问 o1 时,发现 o1 被 t1 锁住了,然后它也等 t1 释放
        
        // 然后就这么干等着...
       t1.start();
       t2.start();
    }
}
5.4 守护线程

java 中线程分为两类:用户线程、守护线程

守护线程的特点:所有的用户线程结束后,守护线程才会自动结束,看视频说守护线程通常是一个死循环

java 复制代码
// 模拟一个守护线程
public class World implements Runnable{

    @Override
    public void run() {
        int i = 0;
        while (true){
            System.out.println(Thread.currentThread().getName() + i);
            i++;
            
            // 延长时间,看的比较直观一些
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

       }
    }
}
 
public class Hello {
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(new World());
        t.setName("守护线程");

        // 设置该线程为守护线程
        t.setDaemon(true);

        t.start();
        
        for (int i = 0; i < 10; i++){
            Thread.sleep(1000);
            System.out.println("main " + i);
        }
    }
}
5.6 定时器

定时器,间隔特定的时间,执行特定的程序。

实现方式,一般都直接使用框架完成

java 底层封装了一个类库:Timer,学习这个类库之后框架也能更好理解

Timer 类的 schedule() 方法需要一个 TimerTask 对象作为第一个参数,表示定时任务的内容

java 复制代码
// 定时器首先需要编写一个定时任务类继承 TimerTask,表明指定时间的任务内容
// 定时任务类
public class World extends TimerTask {

    @Override
    public void run() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = sdf.format(new Date());
        System.out.println("时间是: " + time);
    }
}

// 定时器类:第一种写法
public class Hello {
    public static void main(String[] args) throws ParseException {
        // 创建一个定时器对象,可以指定名字和是否设置为守护线程
        Timer timer = new Timer();
        
        // 日期格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 设置任务第一次执行的时间
        Date firstTime = sdf.parse("2020-7-17 15:36:00");
        
        // 开始定时器,定时器会在指定的时间 (firstTime) 时开始执行定时任务,间隔的时间自己决定
        // new World() 表示定时需要完成的任务
        // firstTime 表示起始时间
        // 1000*3 表示间隔的时间
        timer.schedule(new World(),firstTime,1000*3);
    }
}

// 定时器类:第二种写法 (匿名内部类)
// 可以像上面那样直接写一个定时任务类,也可以像下面这样通过匿名内部类的方式实现
public class Hello {
    public static void main(String[] args) throws ParseException {
        // 创建一个定时器对象,可以指定名字和是否设置为守护线程
        Timer timer = new Timer();
        
        // 日期格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 设置任务第一次执行的时间
        Date firstTime = sdf.parse("2020-7-17 15:36:00");

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // 代码块...
            }
        }, date, 1000 * 3);
    }
}
6、等待和唤醒

wait() 和 notify()

这两个方法是 Object 类自带的,java 中任何一个对象都有这两个方法

wait() 和 notify() 方法建立在线程同步的基础之上,因为他们需要操作同一个仓库,有线程安全问题

  • wait():让正在对象上活动的线程进入等待状态,并释放掉该线程之前占有的对象锁
  • notify():将正在对象上等待的线程唤醒,不会释放该线程之前占有的对象锁
java 复制代码
// 牛奶工厂类
public class MilkFactory {
    
    // 记录牛奶的数目
    private int num;

    public MilkFactory() {
    }

    public MilkFactory(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

// 生产者
public class Producer implements Runnable {

    private MilkFactory factory;

    public Producer(MilkFactory factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        while (true){
            // 将仓库作为锁对象,因为牛奶都存放在这个仓库里,生产者消费者共享这个仓库
            synchronized (factory){
                // 如果仓库有牛奶,那么需要唤醒消费者进行消费
                if (factory.getNum() > 0){
                    try {
                        // 让当前线程等待,即 p 线程等待,同时释放 factory 对象上的锁
                        factory.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 如果仓库没有牛奶,就生产牛奶
                for (int i = 1; i < 6; i++){
                    factory.setNum(i);
                    System.out.println("第" + factory.getNum() + "瓶牛奶生产好了");
                }
                
                // 生产完毕之后唤醒消费者
                // 唤醒正在等待的线程,不会释放锁,程序会往下继续执行
                // 一般将 notify() 写在同步代码块最后,使得唤醒的线程可以马上获得对象锁
                factory.notify();
            }
        }
    }
}

// 消费者
public class Customer implements Runnable {

    private MilkFactory factory;

    public Customer(MilkFactory factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        while (true){
            synchronized (factory){
                // 说明仓库没有东牛奶,需要生产者生产
                if (factory.getNum() == 0){
                    try {
                        // c 线程进入等待状态,释放 factory 对象锁
                        factory.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 仓库有东西,需要消费
                for(int i = 1; i <= factory.getNum(); i++){
                    System.out.println("消费者拿走了第" + i + "瓶牛奶");
                }
                
                // 将仓库牛奶数目置零
                factory.setNum(0);
                
                // 唤醒生产者生产
                factory.notify();
            }
        }
    }
}

// 测试类
public class Hello {
    public static void main(String[] args) {
        MilkFactory factory = new MilkFactory();

        Customer customer = new Customer(factory);
        Producer producer = new Producer(factory);

        Thread c = new Thread(customer);
        Thread p = new Thread(producer);

        c.start();
        p.start();
    }
}

注解

1、Annotation

注解是代码里的一种特殊标记。

它可以在类加载、运行的时候被读取,并执行相应的处理。

2、元注解

在 java.lang.annotation 包下,java 提供了 6 个元注解。

元注解是为了修饰其他的注解。

2.1 @Retention

@Retention 注解只能用来修饰注解,使用的时候需要指定一个 value,来确定它修饰的注解的保留方式

  • RetentionPolicy.CLASS:注解保留在 class 文件中,运行 java 程序时,JVM 不能获取注解的信息
  • RetentionPolicy.RUNTIME:注解保留在 class 文件中,运行 java 程序时,JVM 可以获取注解信息,通过反射也可以
  • RetentionPolicy.SOURCE:注解只保留在源代码中
2.2 @Target

@Target 注解只能用来修饰注解,使用的时候需要指定一个 value 数组,来确定它修饰的注解的使用范围

  • ElementType.ANNOTATION_TYPE:表示它修饰的注解只能修饰注解
  • ElementType.CONSTRUCTOR:表示它修饰的注解只能修饰构造器
  • ElementType.FIELD:表示它修饰的注解只能修饰成员变量
  • ElementType.LOCAL_VARIABLE:表示它修饰的注解只能修饰局部变量
  • ElementType.METHOD:表示它修饰的注解只能修饰方法
  • ElementType.PACKAGE:表示它修饰的注解只能修饰包
  • ElementType.PARAMETER:表示它修饰的注解可以修饰参数
  • ElementType.TYPE:表示它修饰的注解可以修饰类、接口、枚举
2.3 @Documented

@Documented 注解也是用来修饰注解的,直接使用即可

我觉得它的作用不是很大,它主要就是让它修饰的注解,生成 javadoc 文档的时候可以显示出名字,好像就这。

2.4 @Inherited

@Inherited 注解也是修饰注解的,直接使用即可

它的作用就是让它修饰的注解具有继承性,什么意思呢?举个例子看吧

java 复制代码
// 首先定义一个被 @Inherited 修饰的注解
// 这里使用 @Rentention(RetentionPolicy.RUNTIME) 是因为后面用到了反射
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

// 定义一个父类,使用 @MyAnnotation 注解
@MyAnnotation
public class Father{
}

// 子类继承父类
public class Son extends Father{
    public static void main(String[] args){
        // 这个方法是判断 Son 类是否也有 @MyAnnotation 注解修饰
        System.out.println(Son.class.isAnnotationPresent(MyAnnotation.class))
    }
}
3、定义和使用
3.1 定义

通过 @interface 可以自定义注解

java 复制代码
// 自定义注解
public @interface MyAnnotation {
}
3.2 使用

注解几乎可以使用在任何地方,类、接口、注解、方法、变量等等等等

不过通常情况下,不同位置的注解有不同的功能,自定义的时候需要指明使用的范围

java 复制代码
@MyAnnotation
public class Hello {
    
    @MyAnnotation
    int num;
    
    @MyAnnotation
    String s;
    
    @MyAnnotation
    public void m1(@MyAnnotation String name){
        
    }
    
    @MyAnnotation
    public static void m2(){
        @MyAnnotation
        int num = 10;
    }
}
3.3 带成员变量的注解

注解中也可以定义成员变量

java 复制代码
public @interface MyAnnotation{
    // 注解中定义的成员变量以 "返回值 方法名()" 的形式存在
    // 同时可以通过 default 指定默认值
    int age() default 18;
    String name() default "ck";
}

// 定义了成员变量时,使用该注解就需要给成员变量赋值
// 由于没有定义使用范围,所以哪里都可以用
// 这里为成员变量赋值,会覆盖默认值
@MyAnnotation(age = 15)
public class Hello{
    
}
3.4 获取注解信息

使用注解修饰了需要的东西之后,注解不会自己生效,必须由开发商提供的工具处理注解信息。

想要获取注解的信息,可以通过反射来获取

但是要注意,只有使用了 @Retention(RetentionPolicy.RUNTIME) 才能通过反射获取信息

java 复制代码
// 自定义注解
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    int age() default 18;
    String name() default "ck";
}

// 获取注解的信息
public class Hello {

    // 定义方法使用注解
    // 这里不一定是方法,成员变量也可以
    @MyAnnotation
    public void m(){
        System.out.println("12345");
    }

    public static void main(String[] args) throws NoSuchMethodException {
        // 通过反射获取接口数组对象
        Annotation[] ms = Hello.class.getMethod("m").getAnnotations();
        for (Annotation a : ms){
            // 这里强转,是因为 JVM 不知道 a 是什么类型的接口
            System.out.println(((MyAnnotation)a).age() + ((MyAnnotation)a).name());
        }
    }
}
相关推荐
跟着珅聪学java1 小时前
Electron + Vue 现代化“新品展示“和“快捷下单“菜单
开发语言·前端·javascript
是晴天呀。1 小时前
火山引擎接入项目
java·火山引擎
前进的李工1 小时前
数据库视图:数据安全与权限管理利器
开发语言·数据库·mysql·navicat
C_心欲无痕1 小时前
使用 XLSX.js 导出 Excel 文件
开发语言·javascript·excel
sycmancia2 小时前
C++——多态
开发语言·c++
隔壁小邓2 小时前
Spring-全面讲解
java·后端·spring
t198751282 小时前
基于多尺度特征融合与自适应权重优化的水下图像对比度与边缘增强MATLAB方法
开发语言·matlab
JustMove0n2 小时前
互联网大厂Java面试全流程问答及技术详解
java·jvm·redis·mybatis·dubbo·springboot·多线程
SimonKing2 小时前
5分钟学会!把代码从本地推送到 GitHub,就是这么简单
java·后端·程序员