java-基础

又到了快要考计算机二级的日子,本来不想报名的,毕竟用处不大。但是犹豫再三还是报了名,就当做是弥补大学时的遗憾了。

目标:20天完成java基础的系统学习;教材:java核心技术(清华大学出版社)

1. 变量、操作符和语句

1.1 变量

系统为程序分配的一块内存单元,用来存储各种类型的数据。因为该存储单元的数据可能发生改变,因此称为变量

  • 变量分类:

    • 按所属数据类型划分:

      基本数据类型

      引用数据类型

    • 按声明位置划分:

      局部变量:方法或语句块内部定义的变量

      成员变量:方法外部、类的内部定义的变量

注意:类的外面不能有变量的声明

1.2 数据类型

由于记录的数据内容大小不同,所需的存储单元大小也不同,因此使用数据类型类描述

image-20210219212028378

image-20210219212149269

1.3 数据类型转换

boolean类型不能转换为任何其他类型

  • 自动类型转换:容量小的转换成容量大的数据类型

    byte,short,int》long》float》double

    byte,short,int 不会互相转换,它们三者在计算时会转换成 int 类型

  • 强制类型转换:容量大的类型转换成容量小的数据类型时,要加上强制转换符

    1
    2
    long a = 100L;
    int b = (int)a;

    注意:强制转换可能造成精度降低或数据溢出,使用时要小心

  • float变量定义

    1
    2
    float a = 10;//正确
    float b = 10.1; //报错,why?

    float类型变量在定义时需注意:我们直接写出的浮点数字,默认类型是double,会提示需要强转

    解决:在浮点数字后加入f,表示写出的数字是float类型

  • long变量定义

    1
    long a = 2200000000;//报错

    long类型类型变量在定义时需注意:我们直接写出的整形数字,默认类型是int,值超出int范围(稍大于20亿)时报错;

    解决:值后加l

  • 问题:float转long是否可行?

    答:可行,简而言之

    1
    2
    3
    4
    类型			存储				取值范围
    float 4字节 ±3.403E38 有效位数为6~7
    doube 8字节 ±1.78E308 有效位数为15
    long 8字节 ±2^63 (大概是±9E18

    虽然float只有4字节而long有8字节,但是可以看到float的取值范围远大于long(整型依靠位数确定范围,浮点型的位数不直接表示大小,而是以公式的形式计算),注意long类型转float可能存在精度丢失

    float第一位符号位后跟着8位指数域,对应科学计数法的形式,也即float的取值范围是$-2^{128}-2^{128}$。

  • char和int的相互转化:

    • 方法1:

      1
      2
      3
      4
      5
      char ch = '9';
      if (Character.isDigit(ch)){ // 判断是否是数字
      int num = Integer.parseInt(String.valueOf(ch));
      System.out.println(num);
      }
    • 方法2:

      1
      2
      3
      4
      5
      char ch = '9';
      if (Character.isDigit(ch)){ // 判断是否是数字
      int num = (int)ch - (int)('0');
      System.out.println(num);
      }

1.4 语句

  • if判断语句

    else if的含义:if判断通过则不再执行,if没通过才进行else判断,且此时前面的if或else if已经将条件进行筛除

  • switch分支语句

    • 表达式的返回值必须是下述几种类型之一:int, byte, char, short,String;
    • case 子句中的取值必须是常量,且所有 case 子句中的取值应是不同的;
    • case后面跟着的执行体可写可不写
    • default 子句可写可不写
    • break 语句用来在执行完一个 case 分支后使程序跳出 switch 语句块;如果 case 后面没有写 break 则直接往下面执行!
  • while循环和do-while循环的区别

    do-while先执行再判断,while先判断再执行

  • for循环的格式for(一次性执行语句;判断语句;每次都执行的语句){}

    1
    2
    for(System.out.println(1);System.out.println(2);System.out.println(3);){}
    //for循环的执行结果:1只输出一次;之后2,3交替输出
    • 退出多重循环

      1
      2
      3
      4
      5
      haha: for(int i=0; i<4; ++i){
      for(int j=0; j<4; ++j){
      if(j=2) break haha;
      }
      }

1.5 基本输入输出

1.5.1 Scanner 类实现键盘输入

java.util.Scanner

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestInpotScanner {
public static void main(String[] args){
//定义变量,接受用户输入
String key ;
int keyInt;
float keyFloat;
Scanner stdin = new Scanner(System.in); //创建一个扫描器stdin对象
System.out.println("学生的姓名为:");
key = stdin.nextLine(); //收集用户输入
System.out.println("学生的姓名为:"+key);
System.out.println("学生的年龄为:");
keyInt = stdin.nextInt();//获取输入的整形
System.out.println("学生的年龄为:"+keyInt);
System.out.println("学生的成绩为:");
keyFloat = stdin.nextFloat();
System.out.println("学生的成绩为:"+keyFloat);
}
}

1.5.2 JOptionPane对话框输入

JOptionPane是javax.swing中的类,是可视化的对话框。限制了只能输入字符串数据。

实例:

1
2
3
4
5
6
7
8
9
package test01;
import javax.swing.JOptionpane;
public class TestInputJoptionpane{
public static void main(String[] args){
int testInt;
String dataStr;
dataStr = JOptionpane.showInputDialog("输入")
}
}

1.6 各类型数组的默认值

1、int类型定义的数组,初始化默认是0

2、String类型定义的数组,默认值是null

3、char类型定义的数组,使用UTF8字符集 给出的结果是’0’

4、double类型定义的数组,默认值是0.0

5、float类型定义的数组,默认值是0.0

6、boolean类型定义的数组,默认值是false

2. 面向对象

3. 异常

目标:

  • 明确什么异常
  • 能辨识出常见的异常及其含义
  • 理解异常产生的原理
  • 能处理异常
  • 能够自定义异常类型

3.1 异常体系结构

异常是在程序中导致程序中断运行的一种指令流。

异常指的是Exception , Exception类, 在Java中存在一个父类Throwable(可能的抛出)

Throwable存在两个子类:

  • Error:表示的是错误,是JVM发出的错误操作,只能尽量避免,无法用代码处理。
  • Exception:一般表示所有程序中的错误,所以一般在程序中将进行try…catch的处理。

image-20210228132103430

受检和非受检的关系:

  • 非受检的代码:写出来不报错,运行时(比如参数给的不对)就可能报错

    • Exception类的子类:RuntimeException类
  • 受检的代码:写出来就飘红(编译是就报错)

    • 除RuntimeException类之外的其他子类

面试问:java的异常类型

答:受检和非受检

注意观察如下方法的源码:

Integer类: public static int parseInt(String text)throws NumberFormatException

此方法抛出了异常, 但是使用时却不需要进行try。。。catch捕获处理

原因:因为NumberFormatException并不是Exception的直接子类,而是RuntimeException的子类,只要是RuntimeException的子类,则表示程序在操作的时候可以不必使用try…catch进行处理,如果有异常发生,则由JVM进行处理。当然,也可以通过try catch处理

3.2 异常处理

try-catch-finally:

1
2
3
4
5
6
7
8
9
10
11
//如果要想对异常进行处理,则必须采用标准的处理格式,处理格式语法如下:
try{
// 有可能发生异常的代码段
}catch(异常类型1 对象名1){
// 异常的处理操作
}catch(异常类型2 对象名2){
// 异常的处理操作
} ...
finally{
// 异常的统一出口
}

3.2.1 finally的必然执行和常见题目

在进行异常的处理之后,在异常的处理格式中还有一个finally语句,那么此语句将作为异常的统一出口,不管是否产生
了异常,最终都要执行此段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Person haha(){
Person p = new Person();
try{
p.age = 10;
return;
}catch(Exception e){
}finally{
p.age = 20;
}
}
class Person{
int age;
}

问:调用haha方法,上面的finally代码块执行吗?

执行,除了电脑关机断电等直接清空内存的操作,finally必然执行。在return 语句执行结束会等待finally代码块执行,再返回值

输出结果p=20

1
2
3
4
5
6
7
8
9
public int haha(){
try{
int a = 10;
return a;
}catch(Exception e){
}finally{
a = 20;
}
}

此时的返回结果:a = 10;

return a语句执行时会产生一个a的复制最为备份等待返回,尽管执行完finally后a的值为20,但返回的值仍为10。

如何用代码的方式让finally无法执行:System.exit()

在try或catch语句中执行,直接退出JVM,则finally不会执行,这是唯一用代码屏蔽finally的形式

3.2.2 实际的处理流程:

  1. 一旦产生异常,则系统会自动产生一个异常类的实例化对象。

  2. 那么,此时如果异常发生在try语句,则会自动找到匹配的catch语句执行,如果没有在try语句中,则会将异常抛出。

    **抛给调用产生异常的方法的代码位置**

    image-20210228135618027

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    如果main方法中调用了一个f()方法,且因为传入的参数错误导致f()方法内出现异常,则此时的异常抛给调用f()方法的main方法
    main方法可以通过try—catch代码块捕获并处理
    try{
    System.out.println(arr[10]);
    }catch(ArrayIndexOutOfBoundsException e){
    System.out.println("数组没这么长啊");
    }
    如果不处理,则会继续向上抛出给调用main方法的JVM处理
    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10
    at com.java.demo1.Test.main(Test.java:8)
  3. 所有的catch根据方法的参数匹配异常类的实例化对象,如果匹配成功,则表示由此catch进行处理

3.2.3 多异常捕获

多异常捕获的注意点:

  • 捕获更粗的异常不能放在捕获更细的异常之前。
  • 如果为了方便,则可以将所有的异常都使用Exception进行捕获。(利用多态思想)

特殊的多异常捕获写法:

1
2
3
catch(异常类型1 |异常类型2 对象名){
//表示此块用于处理异常类型1 和 异常类型2 的异常信息
}

3.3 throws和throw

3.3.1 throws

异常是抛出还是处理,这是一个问题

对于受检异常,如果能确保正常执行,应该通过throws将异常抛出去

1
2
3
public static void shutdown(String text) throws IOException{
Runtime.getRuntime().exec(text);
}

3.3.2 throw

throw关键字表示在程序中人为的抛出一个异常,因为从异常处理机制来看,所有的异常一旦产生之后,实际上抛出的就是一个异常类的

实例化对象,那么此对象也可以由throw直接抛出。

代码: throw new Exception("抛着玩的。");

很少需要人为的抛出异常,往往通过判断就能避免异常

3.4 自定义异常类 了解

4. 泛型

概念:泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)

作用:

1、 提高代码复用率

2、 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型),比如JDK1.4以前在集合中用Object来接收数据,在调用时又需要强转回原类型

与传参的区别:Java编码惯例,只使用少量的类型参数名称

  • K:键,比如映射的键
  • V:值,比如List和Set的内容
  • E:元素,比如Vector<E>
  • T:泛型,比如上面的Person类

4.1 泛型的使用

4.1.1 泛型类

泛型在类中的使用是最常见的

1
2
3
4
5
6
7
8
9
10
定义一个泛型类:
public class ClassName<T>{
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

实例化时可以将第二个泛型名省略,一个类也可以使用多个泛型

1
ClassName<String> c = new ClassName<>();

4.1.2 泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface IntercaceName<T>{
T getData();
}
实现接口时,可以选择指定泛型类型,也可以选择不指定, 如下:
指定类型:
public class Interface1 implements IntercaceName<String> {
private String text;
@Override
public String getData() {
return text;
}
}
不指定类型:
public class Interface1<T> implements IntercaceName<T> {
private T data;
@Override
public T getData() {
return data;
}
}

4.1.3 泛型方法

1
2
3
4
//有返回值
private static <T> T 方法名(T a, T b) {}
//无返回值
private static <T> void 方法名(T a, T b) {}

可以看出泛型方法的泛型位置在返回值类型声明前,可以作为返回值类型使用,也可以作为参数类型使用

4.2 泛型限制类型

1
2
3
在使用泛型时,可以指定泛型的限定区域,
例如:必须是某某类的子类或 某某接口的实现类,格式:
<T extends 类或接口1 & 接口2>

注意:这里无论是继承类还是实现接口都使用extends关键字,“&”字符连接多个接口

4.3 泛型中的通配符 ?

类型通配符是使用?代替方法具体的类型实参

  • <? extends Parent>指定了泛型类型的上届
  • <? super Child> 指定了泛型类型的下届
  • <?>指定了没有限制的泛型类型
1
2
3
4
5
6
7
8
9
//泛型限制类型的使用
interface Fruit{}
class Apple implements Fruit{}
class Plate<T extends Fruit> {
T data;
}
//测试
Plate<Apple> p = new Plate<>(); //可行
Plate<String> p = new plate<>(); //不可行
1
2
3
4
5
6
7
8
9
10
//泛型通配符的使用
interface Fruit{}
class Apple implements Fruit{}
class Plate<T> {
T data;
}
//测试
Plate<Fruit> p = new Plate<Apple>(); //不可行
Plate<? extends Fruit> p = new Plate<Apple>(); //可行
Plate<? super Apple> p = new plate<Fruit>(); //可行

这里也可以看出泛型限制类型,和泛型通配符的使用区别:

  • 泛型限制类型针对“形参”,在声明类是就限制了能够传入的形参
  • 泛型通配符针对“实参”,在实例对象类型未知时用?代替传入具体类作为实参,也可以对实参类型进行限制

image-20210307142120814

泛型统配符也不能乱用,对于需要传入参数具体类型的add方法在调用时报错,可以看出上述实例对象并未指定准确类型

常见的使用方法:

1
2
3
4
 // 指明泛型参数必须是supC或其子类
public void test( gent<? extends supC> o ) {
System.out.println("Bc");
}

5. 核心类库

5.1 java.util.Objects

父类Object,final修饰

常用静态方法:

  • equals
1
2
3
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
  • isNull
1
2
3
public static boolean isNull(Object obj) {
return obj == null;
}
  • nonNull
1
2
3
public static boolean nonNull(Object obj) {
return obj != null;
}
  • requireNonNull
1
2
3
4
5
6
public static <T> T requireNonNull(T obj) {
if (obj == null)
//如果对象为空,直接抛出异常
throw new NullPointerException();
return obj;
}
  • hash()
1
2
3
4
int code = Objects.hash(Object... values) //为一系列输入值生成哈希码。
public static int hash(Object... values) {
return Arrays.hashCode(values);
}

5.2 java.util.Date

父类java.lang.Object,子类java.sql.Date(缺陷,作用一样)

注意:方法多为过时,只能表示时刻,精确到毫秒

  • 获取当前日期和时间:toString()
  • 获取毫秒数(时间戳):getTime():标准基准时间(1970年1月1日08:00:00,格林尼治标准时间)开始到现在
  • 通过时间戳获取当前日期和时间:setTime()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Date;
public class Date_test {
public static void main(String[] args) {
Date d = new Date();
System.out.println(d);
System.out.println(d.getTime());
d.setTime(d.getTime()-24*60*60*1000);
System.out.println(d);
}
}
//打印结果
Sat Mar 06 10:17:18 CST 2021
1614997038545
Fri Mar 05 10:17:18 CST 2021

5.3 java.text.SimpleDateFormat

java.lang.Object——java.text.Format——java.text.DateFormat(抽象类)——java.text.SimpleDateFormat(实现类)

image-20210306104135339

  • 构造方法:SimpleDateFormat(String pattern)

    String pattern规定了可以解析和输出的时间格式

  • 将声明的Date对象转化为规定格式:String format(Date d)

  • 按规定格式输入时间,获取Date类型的对象:Date parse(String source)
  • 通过Date的getTime方法可以获取时间戳,从而进行时间运算
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Date_test {
public static void main(String[] args) throws ParseException {
SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(s.format(new Date()));
Date d = s.parse("1995-05-29 01:30:30");
long num = new Date().getTime() - d.getTime();
long day = num/(24*60*60*1000);
System.out.println(day);
}
}
//打印结构
2021-03-06 10:54:41
9413

5.4 java.util.Calendar

extends Object, abstract修饰

作用:解决Date类不能国际化的问题,通过时区来进行日历对象的创意

创建对象的方法:Calendar.getInstance()

常用方法:

方法中出现的全局常量,都是对应fields数组(存储本机日历信息)的下标

  • 获取日历对象中的年,月,日,时,分,秒等信息:int get()
  • 不使用当前时间的日历,对信息进行修改:void set()
  • 不使用当前时间的日历,对信息进行修改:void add()

  • 获取日历时间表示的Date对象:Date getTime()

  • 获取各个字段最大的值:int getActualMaxmium
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
//get方法:获取当前日历的信息
int a = c.get(Calendar.MONTH); //MONTH:0-11
int b = c.get(Calendar.DAY_OF_MONTH);
System.out.println(a);
System.out.println(b);
//set方法:修改日历的信息
c.set(Calendar.DAY_OF_MONTH, 1);
b = c.get(Calendar.DAY_OF_MONTH);
System.out.println(b);
//add方法:区别于set的直接修改
c.add(Calendar.DAY_OF_MONTH, 1);
int e = c.get(Calendar.DAY_OF_MONTH);
System.out.println(e);
//getTime方法,返回Date类型
Date d = c.getTime();
System.out.println(d);
//getActualMaximum方法,获取某个信息的最大值
int f = c.getActualMaximum(Calendar.DAY_OF_MONTH);
System.out.println(f);
}
}
//运行结果
2
6
1
2
Tue Mar 02 14:47:32 CST 2021
31

5.5 java.util.Arrays

extends Object

常用静态方法:

  • 获取数组字符串:Arrays.toString(arr)

  • 查找元素下标:Arrays.binarySearch(arr,6)

    Arrays类中定义了一个二分查找的重载方法binarySearch(),从指定的数组范围中查找指定的值,返回其索引位置。二分查找的前提是数组必须有序。如果无序,必须先进行排序,再进行查找。

  • 数组扩容:Arrays.copyOf(arr, 14)

  • 数组排序:Arrays.sort(arr)(还可以通过匿名内部类实现comparable接口,重写compare方法,制定排序规则)

5.6 java.lang.Math

extends Object,final修饰

常用静态方法:

  • 获取绝对值:Math.abs(int/long/float/double a)
  • 最小和最大值:Math.min(), Math.max()
  • 四舍五入:Math.round()
  • 小于等于参数的最大整数:Maht.floor()
  • 大于等于参数的最大整数:Maht.ceil()
  • m的n次方:Math.pow(m,n)

5.7 java.math.BigDecimal

java.lang.Object——java.lang.Number——java.math.BigDecimal

作用:解决double、float数据类型计算时的数据精度问题

常用构造方法:

1
BigDecimal(String a){}

常用方法:

  • 加:BigDecimal add(BidDecimal augend)
  • 减:BigDecimal subtract(BidDecimal augend)
  • 乘:BigDecimal multiply(BidDecimal augend)
  • 除:BigDecimal divide(BidDecimal augend)

返回值仍为BigDecimal类型,需要再新建一个BigDecimal存储

5.8 java.lang.String

extends Object,final修饰

java中的每一个字符串都是一个String的实例对象,且因为String对象时不可变的,所以可以“共享”他们

共享的理解:

相同内容的字符串共享同一块内存地址,存储在字符串常量池中

字符串常量池

1.方法区

方法区,又称永久代(永远存在),被所有线程共享

2.堆

一个JVM实例只存在一个堆内存,堆内存的大小是可调节的。类加载器读了类文件以后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,方便执行器执行

堆在逻辑上分为3部分:

  • 新生代:(又分为Eden区和Survior区),gc方法频繁访问

    • Eden:新创建的对象
    • Survior:经过垃圾回收,但垃圾回收次数少于15次的对象
  • 老年代:垃圾回收次数超过15次仍存活的对象

  • 永久代:不进行垃圾回收(类,方法,常量,静态属性等)

字符串对象就被存在永久代中,每次新创建时在永久代中找有没有相同内容的字符串。

注意:对于new创建的字符串对象,一定是新开了空间,内存地址一定不同

JDK1.7:

字符串常量池:从方法区拿到了堆中

运行时常量池:剩下的东西还在方法区,也就是hotspot的永久代

JDK1.8:

hotspot移除的永久代,用元空间取而代之,字符串常量池还在堆

运行时常量池还在方法区,只不过方法区实现从永久代变为元空间

StringBuilder和StringBuffer

作用:解决String类型字符串拼接无法及时进行垃圾回收的问题

5.9 java.lang.System

6. 集合(java.util包)

6.1 集合的概念

  • 集合的作用:

    将队列,栈,数组,链表,二叉树等数据结构整合为一个个类库,方便数据的管理

  • 集合和数组的异同

    相同点:集合和数组一样,保存的都是一个对象的引用,而不是真正的对象数据,可以实现增删改查等操作

    不同点:

    • 数组的长度是固定的。集合的长度是可变的。
    • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
  • 集合类间的关系

    集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map

image-20210307101805808

img

6.2 Collection接口

Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 java.util.List 和 java.util.Set

  • List 的特点是元素有序、元素可重复。List 接口的主要实现类有 java.util.ArrayListjava.util.LinkedList
  • Set 的特点是元素无序,而且不可重复。Set 接口的主要实现类有java.util.HashSetjava.util.TreeSet

Collection 常用功能

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

需要注意的是:Collection中没有提供获取元素的方法,Set同样如此

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

6.3 List接口

6.3.1 List特点

所有的元素是以一种线性方式进行存储,这也保证了List的特点:

  • 有序(存取都是):例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的
  • 带索引:过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)
  • 允许重复元素:通过元素的equals方法,来比较是否为重复的元素

6.3.2 List重载的方法(基于索引)

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法

  • public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index) :返回集合中指定位置的元素。
  • public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回更新前的元素。
  • public int indexOf(Object o):根据对象查找指定的位置,找不到返回-1
  • List subList(int fromIndex,int toIndex):返回子集合

6.4 List子类

6.4.1 ArrayList

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

ArrayList构造方法:

  • 无参:默认创建的数组长度为10(new的时候初始化长度为0,);
  • 有参:传入int类型的参数,定义长度;

ArrayList扩容算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private Object[] grow() {
return grow(size + 1);
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //原数组长度,扩容肯定是数组满了
int newCapacity = oldCapacity + (oldCapacity >> 1); //先将新长度设置为旧长度的1.5倍
if (newCapacity - minCapacity <= 0) { //如果新长度扔不能满足需要长度,
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) //长度为0(初始化的值)
return Math.max(DEFAULT_CAPACITY, minCapacity); //就把长度设置为(10和需要长度)的较大值
if (minCapacity < 0) // overflow,超出int类型范围,minCapacity为负
throw new OutOfMemoryError();
return minCapacity; //如果添加一组,长度不可控
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}

6.5.2 LinkedList

java.util.LinkedList集合数据存储的结构是双向链表结构。方便元素添加、删除的集合。

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可:

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

6.5 迭代器(Iterator和ListIterator)

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator 。 Iterator 接口也是

Java集合中的一员,但它与 Collection 、 Map 接口有所不同, Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭

代访问(即遍历)Collection 中的元素,因此 Iterator 对象也被称为迭代器。

  • 快速失败:在创建迭代器之后的任何时间修改集合,除了通过迭代器自己的remove方法之外,都会抛出异常
  • 安全失败:迭代器的遍历对象是集合的备份,此时修改集合并不会报错

ListIterator:功能完全相同,只能被List使用

public Iterator iterator() : 获取集合对应的迭代器,用来遍历集合中的元素的

1
2
3
//获取迭代器
List<Integer> l = new ArrayList<Integer>();
Iterator<Integer> i = l.Iterator();

迭代的概念:

迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代

Iterator接口的常用方法如下:

  • public E next() :返回迭代的下一个元素。

  • public boolean hasNext() :如果仍有元素可以迭代,则返回 true。

  • image-20210307151440844

    如图,迭代器操作的就是这个指针,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
List<Integer> l = new ArrayList<>();
l.add(1);
l.add(2);
l.add(3);
l.add(4);
ListIterator<Integer> li = l.listIterator();
li.add(100); //空指针处添加100
li.next(); //下移一位
li.next(); //下移一位
li.set(200); //1的位置改为200
li.previous(); //上移1位
li.previous(); //上移1位
li.previous(); //上移1位(指向100的上面)
while(li.hasNext()){
System.out.println(li.next());
}
}

6.6 增强for(foreach)

格式:

1
2
3
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}

它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。

6.7 Set接口

java.util.Set 接口和 java.util.List接口一样,同样继承自 Collection 接口,它与Collection 接口中的方法基本一致,并没有对

Collection 接口进行功能上的扩充,只是比Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规

则保证存入的元素不出现重复。所有的重复元素依靠 hashCode()和 equals 进行区分。

tips:Set集合取出元素的方式可以采用:迭代器、增强for、调用toArray()方法转成数组。

6.7.1 HashSet

散列存放,利用双列的存储结构HashMap

存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致),底层的实现其实是 java.util.HashMap 支持,根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于: hashCode 与 equals 方法。

只利用Map的键进行存储,值得部分存储特定对象(new Object())

6.7.2 TreeSet

相比HashSet只是增加了排序功能的集合。会按照一定的排序规则,将集合汇总的对象序列时刻按照“升序”排列。所以必须注意的一点是:

  • 向TreeSet添加的对象元素的类型必须实现处于java.lang包下的Comparable接口,否则程序运行时出现java.lang.ClassException异常。API中的String类,封装类都已实现该接口,这个接口只有一个抽象方法:public int compareTo(Object o)用来实现排序规则。

比较器

6.8 Map接口

此接口与 Collection 接口没有任何的关系,是第二大的集合操作接口。此接口常用方法如下:

  • void clear():清空 Map 集合中的内容

  • boolean containsKey(Object key):判断集合中是否存在指定的 key

  • boolean containsValue(Object value):判断集合中是否存在指定的 value

  • **Set> entrySet():将 Map 接口变为 Set 集合**
  • **V get(Object key):根据 key 找到其对应的 value**
  • **V put(K key,V value):向集合中增加内容**
  • void putAll(Map<? extends K,? extends V> m):增加一组集合

  • V remove(Object key):根据 key 删除内容

  • **Set keySet()** **普通** **将全部的 key 变为 Set 集合**

    注意:Map的遍历方法就是通过keySet方法获取key的set集合,然后遍历这一set集合

  • boolean isEmpty():判断是否为空

6.9哈希表理解

对象+数组+链表,在链表长度达到一定程度以后还会转为二叉树

对象:继承自Object的hashCode方法,提供哈希码

数组:默认长度16,通过哈希码%16的方式,获取数组下标存放对象,数组的每个下标被称为哈希桶

链表:当存储的下标出现重复的时候,用链表进行存储

二叉树:JDK1.8之后,当哈希桶中的数据量大于8的时候,链表会转换为红黑树(更便于查找);当哈希桶中的数据量减小到6时,又会转为链表

6.9.1 散列因子(0.75)

为了避免数据过于拥挤,当已存储的哈希桶数量是哈希桶总数(当前的数组长度)的75%时,对数组进行扩容,扩容长度为原来的2倍

散列因子越大,则空间利用率越高,发生哈希碰撞的概率也更高,效率变低

反之则效率变高,即空间和效率的权衡

6.9.2 初始容量(16)

初始容量即创建哈希表时的容量,当哈希表中的散列数超过散列因子和当前容量的乘积时,哈希表将重建内部数据结构(将原先的数据拿出,扩容,再重新添加),如果初始容量不合理,可能会导致大量的时间用于散列

6.10 HashMap

image-20210308110447134

6.11 Map集合的子类区别分析

6.11.1 HashMap,HashTable,ConcurrentHashMap

区别:线程安全与否

HashMap:线程不安全

HashTable:线程安全

ConcurrentHashMap:采用分段锁机制,保证线程安全,效率比较高

6.11.2 TreeMap

和TreeSet类似,增加了排序功能,在操作的时候将按照 key 进行排序,同样要求作为key的类型实现comparable接口

6.11.3 LinkedHashMap

在数据存储到HashMap的同时,还添加到一个双向链表中,这就使得数据存储时可以有序,又兼具哈希表快速检索的优点

6.12 JDK9新特性

创建一个固定长度且不可改变的集合的简便方法:

只有List,Set,Map三个接口下的静态方法,子类均未继承

1
2
3
4
5
6
List<String> l = List.of("123","321");
for(String s: l){
System.out.println(s);
}
123
321

6.13 比较器(comparator和comparable)

与Objects类类似,为了操作集合,有专门的类Collections,存储了用于集合操作的静态方法

1
2
3
4
5
//Collections类
static <T extends Comparable<? super T>> void sort(List<T> list)
//默认将指定列表按升序排序(类内部必须实现Comparable接口)
static <T> void sort(List<T> list, Comparator<? super T> c)
//根据指定比较器引发的顺序对指定列表进行排序。

Arrays.sort()功能与之相同,作用于数组结构

Comparator使用方法:

1
2
3
4
5
6
7
8
9
10
11
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 年龄降序
int result = o2.getAge()-o1.getAge();//年龄降序
if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
result = o1.getName().charAt(0)-o2.getName().charAt(0);
}
return result;
}
});
  • 返回值问题:

    可以将o1,o2理解为左边的元素和右边的元素,如果返回值为-1,则维持原集合,如果返回值为1,则两元素调换位置

    所以当return o2.getAge()-o1.getAge()时,o2大则返回正值,将o2和o1对调(o2在左,o1在右),从左到右为降序

    如果想要升序排列:return o1-o2

Comparable使用方法:

1
2
3
4
5
6
7
8
9
10
public class Person implements Comparable<Person>{
private String name;
private int age;
public int compareTo(Person o){
//返回的数据:负数-this小,0-this和o相同(此时重复),正数-this大
if(this.age<o.age) return -1;
else if(this.age==o.age) return 0;
return 1;
}
}
  • 返回值问题:(和Comparator类比)

    可以将this比作o1,传入的对象比作o2!!!

6.14 equals、hashCode 与内存泄露

存储自定义对象时equals和hashCode的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Book{
Private name;
Private info;
public boolean equals(Object o){
if(this == o){
return true;
}
if(o == null || getClass() != o.getClass()){
return false;
}
Book book = (Book) o;
return Objects.equals(name, book.name) && Objects.equals(info, book.info);
}
public int hashCode(){
return Objects.hash(name, info);
}
}

6.14.1 两个方法的约定

首先必需清楚,当 String 、Math、还有 Integer、Double。。。。等这些封装类在使用 equals()方法时,已经覆盖了 object类的 equals()方法,不再是地址的比较而是内容的比较。

equals方法在重写时必须遵守的规范:

自反性,对称性,传递性,一致性(多次调用结果一致),非空性(非空对象a.equals(null)一定为false)

java.lang.Object 中对 hashCode 的约定(很重要):

  • 如果一个对象的 equals 方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode 方法多次,它必须始终如一地返回同一个整数(一致性)

  • 如果两个对象根据 equals(Object o)方法是相等的,则调用这两个对象中任一对象的 hashCode 方法必须产生相同的整数结果

    (所以修改equals,hashCode方法也要跟着修改)

  • 如果两个对象根据 equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的 hashCode 方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能(哪怕是不着边的两个对象,进行hashCode的时候都可能返回相同结果)

6.4.2 对象相等的判断

在 java 的集合中,判断两个对象是否相等的规则是:

  • 判断两个对象的 hashCode 是否相等

    • 如果不相等,认为两个对象也不相等,完毕

    • 如果相等,转入 2(不能判断就是相等的,还要再判断)

      (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们这里将其做为必需的。后面会重点讲到这个问题。)

  • 判断两个对象用 equals 运算是否相等

    • 如果不相等,认为两个对象也不相等
    • 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)

6.4.3 内存泄漏

对于一个已经被存进 HashSet 集合或Map集合中作为key的对象,不能修改这个对象中参与计算哈希值的字段了,一旦修改,哈希值也被修改,用Contains方法判断元素是否存在或者get(key)方法获取目标对象时必然检索不到,这会导致无法从 HashSet 集合中删除当前对象,从而造成内存泄露

7. 流(java.io)

7.1 概念(p/277)

  • 目的:越出JVM,或者说是内存,与外界进行数据交换。

    也就是将数据传输这一行为抽象为流这个概念,或者也可以具象的将流理解为数据的通道

  • 异常:

    无论是文件对象,还是流对象,在创建时就必须处理IOException

7.2 java.io.File

一个File类的对象就可以抽象的表示一个文件,表示的文件可以不存在,此时创建的对象就可以通过File类的方法进行文件的创建,修改,删除等操作

  • 构造方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //通过路径创建字符串实例
    File(String pathname)
    //通过String parent创建目录,通过child创建文件
    File(String parent, String child)
    //通过File parent创建目录,通过child创建文件
    File(File parent, String child)
    //例子:
    File file = new File("C://haha");
    file.mkdir();
    File txt = new File(file, "1");
    txt.createNewFile();
    File txt2 = new File("C://haha","2.txt");
    txt2.mkdir();
  • 常用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //创建文件 
    File file = new File("C://1.txt");
    boolean flag = file.createNewFile();
    System.out.println(flag? "创建成功""创建失败");
    //创建目录
    File dir = new File("C://1.txt");
    dir.mkdir();

    boolean delete() //删除此抽象路径名表示的文件或目录。

    boolean exists() // 测试此抽象路径名表示的文件或目录是否存在。

    File getAbsoluteFile() //返回此抽象路径名的绝对形式。
    String getAbsolutePath() //返回此抽象路径名的绝对路径名字符串。

    String getName() //返回此抽象路径名表示的文件或目录的名称。
    String getParent() //返回此抽象路径名父项的路径名字符串,如果此路径名未指定父目录,则返回 null 。
    File getParentFile() //返回此抽象路径名父项的抽象路径名,如果此路径名未指定父目录,则返回 null 。
    String getPath() //将此抽象路径名转换为路径名字符串。

    boolean isDirectory() //测试此抽象路径名表示的文件是否为目录。
    boolean isFile() //测试此抽象路径名表示的文件是否为普通文件。

    String[] list() //返回一个字符串数组,用于命名此抽象路径名表示的目录中的文件和目录
    File[] listFiles() //返回一个抽象路径名数组,表示此抽象路径名表示的目录中的文件。

    boolean renameTo(File dest) //重置文件(dest为指定的新抽象路径名)。
  • 文件操作案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class FileTest {
    public static void main(String[] args) throws IOException {
    File file = new File("D://安装包");
    File[] files = file.listFiles();
    listFiles(files);
    }
    public static void listFiles(File[] files){
    if(files == null || files.length == 0) return;
    for(File file: files){
    if(file.isFile()){
    if(file.getName().endsWith(".exe")){
    if(file.length()>200*1024*1024)
    System.out.println(file.getAbsoluteFile());
    }
    }else{
    File[] files1 = file.listFiles();
    listFiles(files1);
    }
    }
    }
    }
  • 过滤器(FileFilter)使用案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class FilterTest {
    public static void main(String[] args) {
    //过滤器的生成方法:FileFilter接口+重写accept方法
    FileFilter filter = new FileFilter(){
    @Override
    public boolean accept(File pathname) {
    if(pathname.getName().endsWith(".exe") || pathname.isDirectory()){
    //返回值为true即代表过滤器选通
    return true;
    }
    //除了上面选通的其他都无法通过
    return false;
    }
    };
    //过滤器的使用场景
    File file = new File("d://安装包");
    File[] files = file.listFiles(filter);//满足过滤器要求的被存入files
    listFiles(files);
    }
    public static void listFiles(File[] files){
    if(files == null || files.length == 0) return;
    for(File file: files){
    if(file.isFile()){
    if(file.length()>200*1024*1024)
    System.out.println(file.getAbsoluteFile());
    }else{
    File[] files1 = file.listFiles();
    listFiles(files1);
    }
    }
    }
    }

7.3 流概述

可以将数据传输操作,看做一种数据的流动 , 按照流动的方向分为输入Input和输出Output

Java中的IO操作主要指的是 java.io包下的一些常用类的使用. 通过这些常用类对数据进行读取(输入Input) 和 写出(输出Output),可以理解为流传输的管道和驱动装置,这里的输入和输出也都是相对于内存而言的!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IO流的分类:
* 按照流的方向来分,可以分为:输入流和输出流.
* 按照流动的数据类型来分,可以分为:字节流和字符流
*
* 字节流:
* - 输入流 : InputStream
* - 输出流 : OutputStream
* 字符流:
* - 输入流 : Reader
* - 输出流 : Writer
*
* 一切皆字节:
* 计算机中的任何数据(文本,图片,视频,音乐等等)都是以二进制的形式存储的.
* 在数据传输时 也都是以二进制的形式存储的.
* 后续学习的任何流 , 在传输时底层都是二进制.

7.4 字节流(byte)

abstract修饰的抽象类,extends Object,输出字节流的所有类的父类,输出流接受输出字节并将他们发送到某个接收器

常用方法:

1
2
3
4
5
6
void write(byte[] b) //将 b.length字节从指定的字节数组写入此输出流。  
void write(byte[] b, int off, int len) //将从偏移量 off开始的指定字节数组中的 len字节写入此输出流。
abstract void write(int b) //将指定的字节写入此输出流。

void close() 关闭此输出流并释放与此流关联的所有系统资源。
void flush() 刷新此输出流并强制写出任何缓冲的输出字节。

写完一定要执行close()将流关闭,不然内存被持续占用,文件无法关闭

字节流的常用子类

7.4.1 FileOutputStream

用于将数据写入文件的管道,写入方法和OutputStream没有区别

构造方法:

1
2
3
FileOutputStream(File file, boolean append) //创建文件输出流以写入由指定的 File对象表示的文件。 

FileOutputStream(String name, boolean append) //创建文件输出流以写入具有指定名称的文件。

常用案例:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws IOException {
FileOutputStream outFile = new FileOutputStream("d://1.txt");
//需要处理FileNotFoundException:虽然没有文件会自动创建,但是有可能因为系统原因创建不了

outFile.write(65);
byte[] b = {65,66,67,68};
outFile.write(b);
outFile.close();
}

注意抛出的异常类型

7.4.2 FileInputStream

构造方法和读取方法与FileOutputStream无异

1
2
3
int read() //从此输入流中读取一个字节的数据。  
int read(byte[] b) //从此输入流将b.length字节的数据读入数组。
int read(byte[] b, int off, int len) //从此off开始将len字节的数据读入一个字节数组。

read()的返回值表示读取的字节值,如果已经读到流末尾再执行read方法,返回值为-1

返回值表示读出的字节长度,如果已经读到流末尾再执行read方法,返回值为-1

1
2
3
4
5
6
7
8
java.io.FileInputStream fis = new java.io.FileInputStream("d://1//1.txt");
int len = fis.read(bytes);
System.out.println(new String(bytes,0,len));
len = fis.read(bytes);
System.out.println(new String(bytes,0,len));
len = fis.read(bytes);
System.out.println(new String(bytes,0,len));
fis.close();

7.4.3 字节输入流的乱码问题

7.4.4 字节流总结

  • 输出流的常用类:FileOutputStream

    声明流对象的构造器的传入参数:写出内容的地址

    write方法的传入参数:需要传输的文件

  • 输出流的常用类:FileOutputStream

    声明流对象的构造器的传入参数:写出内容的地址

    write方法的传入参数:需要传输的文件

7.5 字符流(char)

7.5.1 字符输出流

父类Writer,类似OutputStream是抽象类,常用子类FileWriter

写入的方法:

1
2
3
4
5
void write(char[] cbuf) //写一个字符数组。  
abstract void write(char[] cbuf, int off, int len) //写一个字符数组的一部分。
void write(int c) //写一个字符。
void write(String str) //写一个字符串。
void write(String str, int off, int len) //写一个字符串的一部分

追加的方法:

1
2
3
Writer append(char c) //将指定的字符追加到此writer。  
Writer append(CharSequence csq) //将指定的字符序列追加到此writer。
Writer append(CharSequence csq, int start, int end) //将指定字符序列的子序列追加到此writer。

此方法较为常用,底层实现就是直接调用上面的write方法!!!为什么呢??

因为append的返回值为Writer,于是就可以说实现如下所示的方法

1
2
3
FileWriter fw = new FileWriter("d://1.txt", true);
fw.append("123").append("abc").append("ABC");
fw.close();

构造方法中的boolean append参数和append()方法没啥关系:前者为true,则表示每次生成的输出流都是在原有文件基础上追加;

后者则表示每次写入的内容可以连着追加写入

7.5.2 字符输入流

抽象类:java.io.Reader,常用子类java.io.FileReader

计算机的底层操作本身是以字节为单位进行传输的,字符流这个管道就是通过将传入的单个字节进行缓存,能够拼凑成一个字符时才有资格输出

7.6 flush刷新管道

将管道中的缓存数据直接传出给接收方,比如对于字符流的操作,在使用流进行数据读写出的时候,如果不使用close方法,是看不到数据写到硬盘的。(字节流是不需要的)

执行close()方法时同样是先进行flush()操作

7.7 字节装饰为字符流

使用了:装饰者设计模式

  • 通过字符流的构造方法:以输出流为例

    1
    2
    3
    4
    5
    6
    7
    FileOutputStream fos = new FileOutputStream("d://1.txt");
    /*
    将字节流转为字符流
    参数1.字节流对象
    参数2.指定编码名称
    */
    OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");//Writer抽象类的子类
  • 通过打印流的构造方法:

    1
    2
    3
    4
    FileOutputStream fos = new FileOutputStream("d://1.txt");
    PrintWriter pw= new PrintWriter(fos);
    pw.println("123");
    pw.close();

7.8 打印流及缓存流

7.8.1打印流(写到硬盘)

对于需要输出到本地的时候,推荐使用打印流

同样是进行数据的写出,和字符及字节的输出流一样的功能,也分为字符和字节流

字节打印流:java.io.PrintStream,父类为FileOutputStream

字符打印流:java.io.PrintWriter,父类为Writer

7.8.2 标准IO

其实就是控制台输入输出,java.lang.System的3个静态成员提供了标准输入输出的操作功能

System.out:java.io.PrintStream类型,PrintStream类的write方法本来是写到输出流或数据源中,对于System.out,JVM启动时将其重定向在计算机的终端窗口

System.in:java.io.InputStream类型,本来是从输入流或数据源中读数据,JVM启动时将其重定向在计算机键盘。

7.8.3 缓存流

从输入或者输出流中(而不是文件中)读取文本,缓冲字符,以便有效的读取字符,

以缓存读取流为例:将字符输入流,转换为带有缓存,可以一次读取一行的缓存字符读取流

1
2
3
4
FileReader fw = new FileReader("d://1.txt");
BufferedReader br= new BufferReader(fw);
String textLine = br.readLine();
System.out.println(textLine);

7.9 收集异常日志

1
2
3
4
5
6
7
8
9
10
11
try{
String s = null;
s.toStirng();
}catch(NullPointerException e){
//先创建打印流对象
PrintWriter pw = new PrintWriter("d://1.txt");
simpleDateFormat sdf = new simpleDateFormat("yyyy-MM-dd HH-mm");
System.out.println(sdf.format(new Date()));
e.printStackTrace(pw);
pw.close;
}

7.10 Properties类

java.util包下,父类为java.util.hashMap,对,这个类构建的对象也是一个Map集合

在进行本地存储时需要实现一定的格式,Properties类就是实现这一功能的类之一

1
2
3
4
5
6
7
8
9
10
11
12
Properties ppt = new Properties();
ppt.put("name", "金苹果");
ppt.put("info", "金苹果的种植方法");
PrintWriter pw = new PrintWriter("d://1.txt");
//保存到本地
ppt.store(pw, "this is a book");
pw.close();
//从本地读到内存
FileReader fr = new FileReader("d://1.txt");
ppt.load(fr);
System.out.println(ppt.getProperty("name"));
System.out.println(ppt.getProperty("info"));

输出结果:

image-20210311222542637

7.11 序列化和反序列化

把java对象转换为字节序列,才能实现网络传送,本地存储。把java对象转换为字节序列保存起来的过程称为对象的序列化,再将字节序列恢复为java对象的过程称为反序列化。这里的序列应该是指二进制序列。

方法:

对象要想能够实现序列化,其所属类型必须实现Serializable接口或Externalizable接口

实现方法:

ObjectOutputStream/ObjectInputStream这一对流类,分别是writeObject()方法和readObject()方法

1
2
3
4
5
//对象序列化
OutputStream os = new FileOutputStream(pathname);
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(Student);
oos.close;

7.11.1 部分属性序列化

  • 使用transient修饰符

    在不想被序列化的属性前加上transient修饰符

  • static修饰符

    静态属性被所有对象共享,哪怕对象再序列化之后,操作set方法对静态属性进行修改,此时反序列化的结果即为修改后的结果

  • 在需要序列化的类中添加默认方法writeObject/readObject

    添加的两个方法必须是private修饰

    JVM调用ObjectOutputStream类判断两个添加的方法是否有私有的,无返回值的writeObject方法,如果有,则调用该方法进行序列化

    1
    2
    3
    4
    private void writeObject(ObjectOutputStream oos){
    oos.writeObject(属性1);
    oos.writeObject(属性2);
    }

    即调用对象自有的writeObject方法,想要什么属性序列化就将其添加进去

7.11.2 Externalizable接口

继承自Serializable,使用Externallizable接口必须重写writeExternal和readExternal两个抽象方法,这两个方法其实对应Serializable接口的writeObject和readObject方法。

可以理解为:Externalizable接口的存在就是为了抽象出这两个方法

就像上面的部分属性序列化的方法3:通过重写写入或读出序列的方法,对每个属性单独执行写入或读出操作,即可以用Externalizable来重写

7.11.3 两个序列化接口的关系

区别 Serializable Externalizable
实现复杂度 简单,JVM有内建支持 复杂,有开发人员自己完成
执行效率 所有对象全部保存,效率低 开发人员自己选择保存属性,效率较高
保存信息 空间大 空间小
使用频率

7.12 try-with-resources

1.7之前的IO异常处理try-catch语句块:finally块在try块之外不能使用fr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FileReader fr = null;
try {
fr = new FileReader("c://book.txt");
int c = fr.read();
System.out.println((char)c);
} catch (IOException e) {
e.printStackTrace();
}finally{
try{
fr.close();
}catch(Exception e){
e.printStackTrace();
}
}

1.7:

1
2
3
4
5
6
7
try (FileReader fr = new FileReader("c://book.txt")){
int c = fr.read();
System.out.println((char)c);
fr.close();
} catch (IOException e) {
e.printStackTrace();
}

要求,在try中可以创建对象的类,必须实现Closeable或AutoCloseable接口,运行结束后自动进行关闭操作

JDK9:

1
2
3
4
5
6
7
8
FileReader fr = new FileReader("c://book.txt");
PrintWriter pw = new PrintWriter("c://book.txt");
try(fr;pw){
int c = fr.read();
System.out.println((char)c);
}catch (IOException e) {
e.printStackTrace();
}

7.13 相对路径

java中相对路径的表示:

  1. 以工作目录作为当前目录:

    1
    2
    3
    //当前目录: E://idea-project//day34_xmlAndJson
    //相对路径:src//com//java//demo1
    //表示的绝对路径:E://idea-project//day34_xmlAndJson//src//com//java//demo1
  2. 同样以工作目录作为当前目录

    .表示本目录,..表示父目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class demo1 {
    public static void main(String[] args) throws IOException {
    //得到指向xml文件的输入流
    File f = new File("../idea-exe");
    System.out.println(f.getAbsolutePath());
    System.out.println(f.getCanonicalFile());
    }
    }
    //运行结果:
    D:\idea-project\day34_xmlAndJson\..\idea-exe
    D:\idea-project\idea-exe

注意:工作目录的概念:是程序运行时的起始目录

文件并不一定在当前工程下运行,可能在cmd中的默认路径下用java命令工具执行,这时候的工作目录就是cmd中的默认路径

1
2
3
4
5
6
7
8
9
10
11
12
public class demo1 {
public static void main(String[] args) {
//得到指向xml文件的输入流
File f = new File("src/com/java/demo1");
System.out.println(f.getAbsolutePath());
}
}
//idea中直接运行结果:
D:\idea-project\day34_xmlAndJson\src\com\java\demo1
//cmd中运行结果
C:\Users\biongd>java -jar D:\idea-project\day34_xmlAndJson\out\artifacts\day34_xmlAndJson_jar\day34_xmlAndJson.jar
C:\Users\biongd\src\com\java\demo1

可以看出其工作目录就是运行目录

7.14 总结

修改源文件中某一行的内容

  1. 输出流在建立的时候会将文件清空,在创建File的对象的时候传入参数true可以保留之前内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void setText(File f,String target,String src) throws IOException {//修改
BufferedReader br= new BufferedReader(new FileReader(f));
PrintWriter pw= new PrintWriter(new FileWriter(f,true));
try(br;pw){
StringBuffer sb = new StringBuffer();
String huanhang =System.getProperty("line.separator");//平台换行!
while(true){
String line = br.readLine();
if(line == null) break;
if(line.contains(target)){
line = src;
}
sb.append(line+huanhang);

}
System.out.println(sb);
String a = sb.toString();
pw.println(a);
pw.close();
}catch(Exception e){
e.printStackTrace();
}
}

源文件:

1
1234

输出结果:

1
2
1234
abcd
  1. 先全部读出,完成修改并保存内容到内存后再建立输出流,此时就可以清空内容再写入
1
2
3
4
String a = sb.toString();
PrintWriter pw= new PrintWriter(new FileWriter(f,true));
pw.println(a);
pw.close();

8. 多线程

8.1 线程和进程

进程的概念:操作系统-进程

8.1.1 进程和线程的关系:

  • 进程

    一个内存中运行的应用程序,每个应用程序都有独立的内存空间

  • 线程

    是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程

    线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

  • 两者的关系

    其实上面包括我之前的文章总结一下就很好理解:进程是建立在线程之上的一个空泛的概念,在任务管理器中看到的进程实际上底层在执行的是实现这个应用程序功能的所有线程!进程就类似于一个接口,而线程就是能够具体实现的子类!

    所以:

    • 进程是系统资源分配的单位
    • 线程是CPU调度和执行的单位
    • 一个单核的cpu可以并发的执行多条进程,但是这本质上也是从一个进程的一个线程跳到另一个进程的一个线程!!!!!

    注意:

    • 线程没有独立的存储空间,一个进程下的所有线程共享
    • 线程之间的切换只需要切换执行流程和相关的局部变量,这种切换要比进程之间的切换效率高得多

8.1.2 线程调度

同样参考上面进程的文章,里面有介绍进程的cpu资源分配方法,其实也就是线程的调度方法

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高

这句话该如何理解呢:

程序计数器只有一个,所以程序并发的执行速度和顺序执行的速度并没有什么区别,反而是切换会导致耗时

但是以一个应用程序的登录界面为例,线程需要接收用户输入才能继续执行,总不能用户不输入程序就崩溃了吧

8.2 在java中实现多线程

8.2.1 继承Thread类

  • 创建一个类继承Thread类,此时继承了Thread的子类就是一个线程类
  • 重写run方法public void run(),这个方法的内容就是另外的一条执行路径
  • 这条执行路径的出发方式:线程类的start()方法!
  • main方法中可以有多条执行路径,只有当所有的路径全部结束,程序才算执行完毕

注意:

每个路径都有自己的栈空间,共享同一块堆空间!!每条路径调用的方法也都在其内部执行,执行完后将结果弹出到main

8.2.2 实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyRunnable{
@Override
public void run(){
for(int i=0; i<10; ++i){
System.out.println(i);
}
}
}
//创建一个任务对象
MyRunnable mr = new MyRunnable();
//创建一个线程,并为其分配一个任务
Thread t = new Thread(mr);
//执行这个线程
t.start();

其实,继承Thread类来实现多线程,其实是相当于拿出三件事即三个卖早餐10份的任务分别分给三个窗口,他们各做各的事各卖各的早餐各完成各的任务,因为MyThread继承Thread类,所以在newMyThread的时候在创建三个对象的同时创建了三个线程;

实现Runnable的, 相当于是拿出一个卖早餐10份的任务给三个人去共同完成,newMyThread相当于创建一个任务,然后实例化三个Thread,创建三个线程即安排三个窗口去执行。

  • 相比于继承Thread的优点
    • 更适合多个线程同时执行相同任务的情况
    • 可以避免单继承带来的局限性
    • 任务于线程本身分离,提高了代码的健壮性
    • 后续学习的线程池,接受Runnable类型,不接受Thread类型

8.2.3 实现Callable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}

2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象:
FutureTask<Integer> future = new FutureTask<>(callable);

3. 通过Thread,启动线程
new Thread(future).start();

和Runnable相比,实现Callable接口的区别就在于有一个返回值,并且可以通过get方法获取到这个返回值,在执行get()方法时候,主线程会等待返回值获取到之后才继续进行。

8.3 Thread类

构造方法:

1
2
3
4
Thread() //分配新的 Thread对象。  
Thread(Runnable target) //传入任务参数
Thread(Runnable target, String name) //传入任务参数和名称
Thread(String name) //只传入名称

优先级字段:

1
2
3
static int MAX_PRIORITY 线程可以拥有的最大优先级。  
static int MIN_PRIORITY 线程可以拥有的最低优先级。
static int NORM_PRIORITY 分配给线程的默认优先级。

常用方法:

1
2
3
4
5
6
7
8
9
void setPriority(int newPriority) 更改此线程的优先级。  
int getPriority() 返回此线程的优先级。
static Thread currentThread() 返回对当前正在执行的线程对象的引用。
long getId() 返回此Thread的标识符。
String getName() 返回此线程的名称。
void setName​(String name) 将此线程的名称更改为等于参数 name 。
static void sleep​(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
static void sleep​(long millis, int nanos) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。
oolean isDaemon() 测试此线程是否为守护程序线程。

8.3.1 获取和设置线程名称

1
2
3
4
5
6
7
8
9
10
11
12
class MyRunnable{
@Override
public void run(){
System.out.println("Thread.currentThread().getName()");
}
}
MyRunnable mr = new MyRunnable();
new Thread(mr).start();
//或者
Thread t = new Thread(mr);
t.setNmae("123");
t.start();

8.4 线程状态及守护线程

  • 线程休眠static void sleep()

  • 线程阻塞

    可以理解为所有的耗时操作,比如文件的输入输出,就只有等待文件完全读取,线程才能继续执行,这里的等待就被称为线程阻塞

  • 线程中断

    一个线程是一个独立的路径,它是否应该结束,应该由它自己决定

    线程在执行过程中会占用很多资源,比如硬件资源在未完成时突然中断,可能会导致占用无法释放或者垃圾无法回收

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static void main(String[] args){
    Thread t = new Thread(new MyRunnable());
    for(int i=0; i<5; ++){
    System.out.println(i);
    }
    t.interrupt();//该方法用于给线程添加标记,部分方法在添加标记后执行就会抛出异常
    }
    static class MyRunnable{
    public void run(){
    for(int i=0; i<5; ++){
    System.out.println(i);
    }
    try{
    Thread.sleep(1000);
    }catch(InterruptException e){
    //在添加标记后,再执行该线程的sleep方法就会进入该catch块
    //此时可以决定是否让线程继续!
    //想要让线程结束就直接return,结束run方法
    }
    }
    }

    所以想要让线程中断就可以在catch块内将资源的释放等操作进行执行,最后再return

  • 守护线程

    线程的分类:守护线程和用户线程

    用户线程:当一个进程的所有用户线程结束,则进行结束

    守护线程:守护用户线程,当最后一个用户线程结束后,所有守护线程自动死亡

8.5 线程安全问题-锁

线程不安全的产生原因:

  • 当多个线程对同一个数据进行操作时(比如实现Runnable接口的任务类被多个线程公用),此时无法保证线程在进入时拿到的数据和处理时使用的数据一致(有可能执行中途被别的线程获取时间片,修改数据内容),导致结果不符合预期

解决线程不安全的方向:

  • 对于被争抢的数据所在的执行代码块,使其排队执行,即在一个线程执行的时候,其余线程排队

8.5.1 方法1-同步代码块

格式:synchronized(锁对象){同步代码块}

锁对象:任何对象都可以作为锁存在(可以打上锁的标记),比如可以传入一个Object对象

锁对象的作用:同时有a,b,c 3个线程执行之一同步代码块,a执行时就会给Object对象加上锁的标记(硬件实现),此时b,c无法执行,a执行结束后,锁标记释放;此时b,c检查到后看是争抢Object对象,争抢到后给对象添加锁标记,开始执行

注意:想要同步的线程必须用同一个锁对象(共用一把锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyRunnable{
private int count = 20;
private Object o = new Object();
@Override
public void run(){
while(true){
synchronized(o){
if(count>0){
count--;
System.out.println(count);
}
}
}
}
}

可以看到:当有一个线程开始执行同步代码块,此时别的线程就只能停留在while处等待该线程执行完,

8.5.2 方法2-同步方法

给方法添加synchronized的修饰符

同步方法同样有锁,锁对象就是this,也就是同步方法所在的对象!

对于静态方法而言,锁的对象就是当前所在的类:类名.class

8.5.3 方法3-显示锁

上述的同步代码块和同步方法都属于隐式锁

显示锁:Lock类,用的是其子类ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyRunnable implements Runnable{
private int count = 20;
private Lock l = new ReentrantLock;
@Override
public void run(){
l.lock();
while(true){
synchronized(o){
if(count>0){
count--;
System.out.println(count);
}
}
}
l.unlock();
}
}

再次强调: 不管显式锁和隐式锁,对于执行同一任务的线程必须持有同一把锁(锁的声明必须在任务类中的run方法外)!!!

问:隐式锁和显式锁的区别

8.5.4 公平锁和非公平锁

公平锁和非公平锁的区别就在于线程执行顺序的问题

  • 公平锁:先到先得,一个锁释放,就由先执行到这一步的线程(比如while循环,排在最前面的线程)来获取锁
  • 非公平锁:抢占式,那个线程抢到那个执行,一般刚刚持有锁的执行完后再获得锁的概率很高

8.5.5 线程死锁

一个加锁(Lock1)的代码块内又有一个加锁(Lock2)的代码块,而Lock2的内部又有Lock1,此时双方都要等待对方的锁释放获得资源才能继续执行,类似于锁存器

https://blog.csdn.net/weixin_43213517/article/details/90314004

8.6 多线程通信问题

问:Object的wait方法和Thread的sleep方法有何区别

生产者和消费者问题:

通过Object的wait方法和notify方法实现线程的等待和唤醒,需要注意的是:

调用obj的wait(), notify()方法前,必须获得锁!!!

要么必须写在synchronized(obj) 代码段内;要么是在synchronized声明的方法中用隐式的锁对象this调用

8.7 线程的状态

枚举类java.lang.Enum<Thread.State>下的Thread.state

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程状态图

8.8 线程池Executors

  • 为什么需要线程池

    如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

  • 线程池的好处

    • 降低资源消耗。
    • 提高响应速度。
    • 提高线程的可管理性

8.8.1 缓存线程池

特点:长度无限制

执行流程:

    1. 判断线程池是否存在空闲线程
    1. 存在则使用
    1. 不存在,则创建线程 并放入线程池, 然后使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
Thread.sleep(3000);//如果1,2任务未执行完则执行第三个任务时会在缓存线程池中再创建一个线程执行该任务,但是通过加了个等待时间,则下面的任务可以使用上面结束的线程
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});

8.8.2 定长线程池

指定线程池长度

执行流程:

    1. 判断线程池是否存在空闲线程
    1. 存在则使用
    1. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
    1. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
1
2
3
4
5
6
7
8
9
10
11
12
13
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});

如上,新建线程池的方法中传入线程池的长度参数为2 ,则最多只能有两个线程被创建,如果两个线程都在执行,则更多的任务只能等待!!

8.8.3 单线程

效果与定长线程池 创建时传入数值1 效果一致.

执行流程:

    1. 判断线程池 的那个线程 是否空闲
    1. 空闲则使用
    1. 不空闲,则等待 池中的单个线程空闲后 使用
1
2
//创建线程池的方法
ExecutorService service = Executors.newSingleThreadExecutor();

8.8.4 周期性任务定长线程池

执行流程:

    1. 判断线程池是否存在空闲线程
    1. 存在则使用
    1. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
    1. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

定时执行:当某个时机触发时, 自动执行某任务

定时执行1次

  • 参数1. runnable类型的任务
  • 参数2. 时长数字(即在这个时间后执行)
  • 参数3. 时长数字的单位
1
2
3
4
5
6
7
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);

周期执行任务:

  • 参数1. runnable类型的任务
  • 参数2. 时长数字(第一次执行在多次时间以后)
  • 参数3. 周期时长(每次执行的间隔时间)
  • 参数4. 时长数字的单位
1
2
3
4
5
6
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);

8.9 lambda表达式

函数式编程思想:和面向对象是有些对立的思想

格式:

(参数) -> {方法}

1
2
3
4
5
6
7
8
9
//冗余的线程实现写法:
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
//替换为lambda表达式
service.scheduleAtFixedRate(() ->{System.out.println("俩人相视一笑~ 嘿嘿嘿"), 5, 2}

案例2:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args){
print((int a, int b) -> { //将new 父类/接口()跟匿名内部类的方式用lambda的方式表现出来
return a+b;
}, 100, 200);
}
interface Sum{
int sum(int a, int b);
}
public static print(Sum s, int a, int b){
int num = s.sum(a,b);
System.out.println(num);
}

可以看到,lambda公式是在传递实参时,需要新建对象(继承自抽象父类或接口)且需要重写方法的场合!!

相对于匿名内部类的方式,lambda只需要考虑需要重写的方法(因为类名作为形参已经声明了),括号内为重写方法的参数,箭头后面是方法的内容!!

9. 网络编程

9.1 网络知识

https://biongd.gitee.io/2020/10/23/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/

9.2 TCP程序

网络编程的分类:

  1. B/S 程序 : 浏览器与服务器程序
  2. C/S 程序 : 客户端与服务器程序

9.2.3 C/S程序

需要使用到两个类:

  1. ServerSocket 搭建服务器
  2. Socket 搭建客户端
  3. 两方使用socket(套接字 , 通信端点) 进行交流

服务器端:ServerSocket类

用于创建服务器 . 创建完毕后, 会绑定一个端口号。然后此服务器可以等待客户端连接 。每连接一个客户端 , 服务器就会得到一个新的Socket对象,,用于跟客户端进行通信 。

  • 构造方法:
1
2
ServerSocket(int port); 
//创建一个基于TCP/IP协议的服务器 , 并绑定指定的端口号.

注意: 参数port的范围是: 0-65535 (建议1025-65535)

  • 常用方法:

Socket accept():等待客户端连接 ,此方法会导致线程的阻塞!直到一个新的客户端连接成功, return Socket对象后, 线程才能继续执行。
void close():释放占用的端口号 , 关闭服务器.

客户端:Socket

是两台计算机之间通信的端点 , 是网络驱动提供给应用程序编程的一种接口,一套标准,一种机制

  • 构造方法:
1
2
3
4
Socket(String ip,int port)
//创建一个套接字, 并连接指定ip和端口号的 服务器.
//参数1. 服务器的ip地址
//参数2. 服务器软件的端口号..
  • 常用方法:

OutputStream getOutputStream():返回的是 , 指向通信的另一端点的输出流

InputStream getInputStream():返回的是 , 指向通信的另一端点的输入流

void close():关闭套接字
总结:

服务器就像是客栈,是必须有明确的地址和大门的实体!客户端就像是来吃饭住宿,只要知道地址和大门就行了,无需实例化

10. IDEA

10.1 debug

1
2
3
4
5
6
7
8
9
public class debugTestTest {
public static void main(String[] args) {
int s = 0;
for(int i =0; i<10; ++i){
//在这一行设置断点 ++s;
}
System.out.println(s);
}
}
  • 设置条件:

    红点右键:比如condition设置为s == 5,表示满足s==5这一条件时才会触发断点

image-20210321223354497

  • 左侧红圈:

    • 第一个:再次执行到断点处
    • 第二个:暂停
    • 第三个:查看所有已添加的断点信息
    • 第四个:屏蔽所有断点
  • 上面红圈:

    • 将光标移动到当前运行到的代码位置

    • 单步跳过

    • 进入自定义方法
    • 进入所有方法(包括自定义的和系统的)
    • 退出本层方法
    • 退出全部方法至最顶层
    • 程序直接执行到光标处

10.2 junit(单元检测)

问:jar文件是干嘛的?

jar包就是别人已经写好的一些类,然后将这些类进行打包,你可以将这些jar包引入你的项目中,然后就可以直接使用这些jar包中的类和属性以及方法。

JAR(Java ARchive)是将一系列文件合并到单个压缩文件里,就象Zip那样。然而,同Java中其他任何东西一样,JAR文件是跨平台的,所以不必关心涉及具体平台的问题。
涉及因特网应用时,JAR文件显得特别有用。在JAR文件之前,Web浏览器必须重复多次请求Web服务器,以便下载完构成一个“程序片”(Applet)的所有文件。除此以外,每个文件都是未经压缩的。但在将所有这些文件合并到一个JAR文件里以后,只需向远程服务器发出一次请求即可。同时,由于采用了压缩技术,所以可在更短的时间里获得全部数据

通过将jar文件导入项目,即可以通过快捷键直接生成单元的测试文件ctrl+shift+t

导入方法见文件:java-IDEA

1
2
3
4
5
6
7
8
9
10
11
public class Demo {
public void haha(){
System.out.println("haha");
}
public void heihei(){
System.out.println("heihei");
}
public int sum(int x,int y){
return x*y;
}
}

生成测试文件,写入验证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static org.junit.Assert.*;
public class DemoTest {
@Test
public void sum(){
int sum = new Demo().sum(100,100);
Assert.assertEquals(200,sum);//断言
}
@Test
public void haha() {
new Demo().haha();//测试
}
@Test
public void heihei() {
new Demo().heihei();
throw new RuntimeException("出异常了");
}
@Test
public void haHei(){
new Demo().haha();
new Demo().heihei();
}
}

断言方法的使用效果

Annotation 2020-08-13 132754.png

11. XML和JSON

11.1 XML概念

11.1.1 XML简介

可扩展标记语言(eXtensible Markup Language)。

  • 特性:
  1. xml具有平台无关性, 是一门独立的标记语言.
  2. xml具有自我描述性
  • XML的作用:
  1. 网络数据传输(socket)
  2. 数据存储(由专门的存储工具)
  3. 配置文件(只有这个方法是最常用的)
  • XML文件
  1. .XML文件是保存XML数据的一种方式
  2. XML数据也可以以其他的方式存在(如在内存中构建XML数据)。
  3. 不要将XML语言狭隘的理解成XML文件。

11.1.2 XML语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
1. XML文档声明
<?xml version="1.0" encoding="UTF-8"?>

2. 标记 (也被叫做:元素 / 标签 / 节点)
XML文档,由一个个的标记组成.
语法:
开始标记(开放标记): <标记名称>
结束标记(闭合标记): </标记名称>
标记名称: 自定义名称,必须遵循以下命名规则:
1.名称可以含字母、数字以及其他的字符
2.名称不能以数字或者标点符号开始
3.名称不能以字符 “xml”(或者 XML、Xml)开始
4.名称不能包含空格,不能包含冒号(:)
5.名称区分大小写
标记内容: 开始标记与结束标记之间 ,是标记的内容.
例如 ,我们通过标记, 描述一个人名:
<name>李伟杰</name>

3. 一个XML文档中, 必须有且且仅允许有一个根标记.
正例:
<names>
<name>张三</name>
<name>李四</name>
</names>
反例:
<name>李四</name>
<name>麻子</name>

4. 标记可以嵌套, 但是不允许交叉.
正例:
<person>
<name>李四</name>
<age>18</age>
</person>
反例:
<person>
<name>李四<age></name>
18</age>
</person>

5. 标记的层级称呼 (子标记, 父标记 , 兄弟标记, 后代标记 ,祖先标记)
例如:
<persons>
<person>
<name>李四</name>
<length>180cm</length>
</person>
<person>
<name>李四</name>
<length>200cm</length>
</person>
</persons>
name是person的子标记.也是person的后代标记
name是persons的后代标记.
案例:
语法进阶CDATA (了解)
name是length的兄弟标记.
person是name的父标记.
persons是name的祖先标记.
6. 标记名称 允许重复
7. 标记除了开始和结束 , 还有属性.
标记中的属性, 在标记开始时 描述, 由属性名和属性值 组成.
格式:
在开始标记中, 描述属性.
可以包含0-n个属性, 每一个属性是一个键值对!
属性名不允许重复 , 键与值之间使用等号连接, 多个属性之间使用空格分割.
属性值 必须被引号引住.
案例:
<persons>
<person id="10001" groupid="1">
<name>李四</name>
<age>18</age>
</person>
<person id="10002" groupid="1">
<name>李四</name>
<age>20</age>
</person>
</persons>
8. 注释
注释不能写在文档文档声明前
注释不能嵌套注释
格式:
注释开始: <!--
注释结束: -->
  • 语法进阶CDATA
1
2
3
4
5
6
7
8
9
10
11
CDATA
CDATA 是不应该由 XML 解析器解析的文本数据。
像 "<" 和 "&" 字符在 XML 元素中都是非法的。
"<" 会产生错误,因为解析器会把该字符解释为新元素的开始。
"&" 会产生错误,因为解析器会把该字符解释为字符实体的开始。
Java解析XML 掌握
面试题 *
某些文本,比如 JavaScript 代码,包含大量 "<" 或 "&" 字符。为了避免错误,可以将脚本代
码定义为 CDATA
CDATA 部分中的所有内容都会被解析器忽略。
CDATA 部分由 "<![CDATA[" 开始,由 "]]>" 结束:

11.2 Java解析XML

11.2.1 Java中有几种XML解析方式

  • SAX解析

    解析方式是事件驱动机制 !

    SAX解析器, 逐行读取XML文件解析 , 每当解析到一个标签的开始/结束/内容/属性时,触 发事件. 我们可以编写程序在这些事件发生时, 进行相应的处理.

    优点:

    • 分析能够立即开始,而不是等待所有的数据被处理
  • 逐行加载,节省内存.有助于解析大于系统内存的文档
  • 有时不必解析整个文档,它可以在某个条件得到满足时停止解析.

缺点:

  • 单向解析,无法定位文档层次,无法同时访问同一文档的不同部分数据(因为逐 行解析, 当解析第n行是, 第n-1行已经被释放了, 无法在进行操作了).
  • 无法得知事件发生时元素的层次, 只能自己维护节点的父/子关系.
  • 只读解析方式, 无法修改XML文档的内容.
  • DOM解析

    是用与平台和语言无关的方式表示XML文档的官方W3C标准,分析该结构通常需要加载整个 文档和内存中建立文档树模型.程序员可以通过操作文档树, 来完成数据的获取 修改 删除等.

    优点:

    • 文档在内存中加载, 允许对数据和结构做出更改.

    • 访问是双向的,可以在任何时候在树中双向解析数据。

    缺点:

    • 文档全部加载在内存中 , 消耗资源大.
  • JDOM解析

    目的是成为Java特定文档模型,它简化与XML的交互并且比使用DOM实现更快。由于是第一 个Java特定模型,JDOM一直得到大力推广和促进。

  • JDOM文档声明其目的是“使用20%(或更少)的精力解决80%(或更多)Java/XML问题” (根据学习曲线假定为20%)

    优点:

    • 使用具体类而不是接口,简化了DOM的API。

    • 大量使用了Java集合类,方便了Java开发人员。

    缺点:

    • 没有较好的灵活性。
  • 性能不是那么优异。
  • DOM4J解析

    它是JDOM的一种智能分支。它合并了许多超出基本XML文档表示的功能,包括集成的XPath 支持、XML Schema支持以及用于大文档或流化文档的基于事件的处理。它还提供了构建文档表示的选项, DOM4J是一个非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一 个开放源代码的软件。如今你可以看到越来越多的Java软件都在使用DOM4J来读XML。 目前许多开源项目中大量采用DOM4J , 例如:Hibernate

11.2.2 DOM4J解析XML掌握

  • 步骤:
1
2
3
4
5
6
7
8
9
1. 引入jar文件 dom4j.jar 
2. 创建一个指向XML文件的输入流
FileInputStream fis = new FileInputStream("xml文件的地址");
3. 创建一个XML读取工具对象
SAXReader sr = new SAXReader();
4. 使用读取工具对象, 读取XML文档的输入流 , 并得到文档对象
Document doc = sr.read(fis);
5. 通过文档对象, 获取XML文档中的根元素对象
Element root = doc.getRootElement();
  • 文档对象 Document
1
2
3
4
5
指的是加载到内存的 整个XML文档. 常用方法: 
1. 通过文档对象, 获取XML文档中的根元素对象
Element root = doc.getRootElement();
2. 添加根节点
Element root = doc.addElement("根节点名称");
  • 元素对象 Element
1
2
3
4
5
6
7
8
9
10
指的是XML文档中的单个节点. 常用方法: 
1. 获取节点名称 String getName();
2. 获取节点内容 String getText();
3. 设置节点内容 String setText();
4. 根据子节点的名称 , 获取匹配名称的第一个子节点对象. Element element(String 子节点名称);
5. 获取所有的子节点对象 List<Element> elements();
6. 获取节点的属性值 String attributeValue(String 属性名称);
7. 获取子节点的内容 String elementText(String 子节点名称);
8. 添加子节点 Element addElement(String 子节点名称);
9. 添加属性void addAttribute(String 属性名,String 属性值);
  • 解析本地文件案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1. 获取文件的输入流 
FileInputStream fis = new FileInputStream("C:\\code\\35\\code1\\day03_XML\\src\\books.xml");
//2. 创建XML读取工具对象
SAXReader sr = new SAXReader();
//3. 通过读取工具, 读取XML文档的输入流 , 并得到文档对象
Document doc = sr.read(fis);
//4. 通过文档对象 , 获取文档的根节点对象
Element root = doc.getRootElement();
//5. 通过根节点, 获取所有子节点
List<Element> es = root.elements();
//6. 循环遍历三个
book for (Element e : es) {
//1. 获取id属性值
tring id = e.attributeValue("id");
//2. 获取子节点name , 并获取它的内容
String name = e.element("name").getText();
//3. 获取子节点info , 并获取它的内容
String info = e.element("info").getText();
System.out.println("id="+id+",name="+name+",info="+info); }
  • 解析网络文件案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String phone = "18516955565"; 
//1. 获取到XML资源的输入流 URL
url = new URL("http://apis.juhe.cn/mobile/get? phone="+phone+"&dtype=xml&key=9f3923e8f87f1ea50ed4ec8c39cc9253");
URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream();
//2. 创建一个XML读取对象
SAXReader sr = new SAXReader();
//3. 通过读取对象 读取XML数据,并返回文档对象
Document doc = sr.read(is);
//4. 获取根节点
Element root = doc.getRootElement();
//5. 解析内容
String code = root.elementText("resultcode");
if("200".equals(code)){ Element result = root.element("result");
String province = result.elementText("province");
String city = result.elementText("city");
if(province.equals(city)){
System.out.println("手机号码归属地为:"+city);
}else{
System.out.println("手机号码归属地为:"+province+" "+city);
}
}else{
System.out.println("请输入正确的手机号码");
}

总结:无论怎样都得先获取根节点,然后才能一级一级的获取元素的名称或内容等属性

11.2.3 DOM4J - XPATH解析XML

不需要一级一级的向下遍历,通过路径进行获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过路径快速的查找一个或一组元素 路径表达式: 
1. / : 从根节点开始查找
2. // : 从发起查找的节点位置 查找后代节点 ***
3. . : 查找当前节点
4. .. : 查找父节点
5. @ : 选择属性. * 属性使用方式: [@属性名='值'] [@属性名>'值'] [@属性名<'值'] [@属性名!='值']
例如
books: 路径: //book[@id='1']//name
books
book id=1
name
info
book id=2
name
info
  • 本地解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo3 {
public static void main(String[] args) throws IOException, DocumentException {
//1. 获取输入流
FileInputStream fis = new FileInputStream("c://Demo1.xml");
//2. 创建XML读取对象
SAXReader sr = new SAXReader();
//3. 读取并得到文档对象
Document doc = sr.read(fis);
//4. 通过文档对象+xpath,查找所有的name节点
/*List<Node> names = doc.selectNodes("//book[@id='1001']//name");//Node:文档对象和元素对象都是他的子
for (int i=0;i<names.size();i++){
System.out.println(names.get(i).getName());
System.out.println(names.get(i).getText());
}*/
Node n = doc.selectSingleNode("//book[@id='1002']//name");
System.out.println(n.getName()+":"+n.getText());
fis.close();
}
}
  • 网络解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String phone = "18516955565"; 
//1. 获取到XML资源的输入流
URL url = new URL("http://apis.juhe.cn/mobile/get?phone="+phone+"&dtype=xml&key=9f3923e8f87f1ea50ed4ec8c39cc9253");
URLConnection conn = url.openConnection();
InputStream is = conn.getInputStream();
//2. 创建一个XML读取对象
SAXReader sr = new SAXReader();
//3. 通过读取对象 读取XML数据,并返回文档对象
Document doc = sr.read(is);
//4. 获取根节点
Element root = doc.getRootElement();
//5. 解析内容
String code = root.elementText("resultcode");
if("200".equals(code)){
Element result = root.element("result");
String province = result.elementText("province");
String city = result.elementText("city");
if(province.equals(city)){
System.out.println("手机号码归属地为:"+city);
}else{System.out.println("手机号码归属地为:"+province+" "+city);
}
}else{
System.out.println("请输入正确的手机号码");
}

11.3 XML生成

步骤:

  1. 通过文档帮助器 (DocumentHelper) , 创建空的文档对象
    Document doc = DocumentHelper.createDocument();
  2. 通过文档对象, 向其中添加根节点
    Element root = doc.addElement("根节点名称");
  3. 通过根节点对象root , 丰富我们的子节点
    Element e = root.addElement("元素名称");
  4. 创建一个文件输出流 ,用于存储XML文件
    FileOutputStream fos = new FileOutputStream("要存储的位置");
  5. 将文件输出流, 转换为XML文档输出流
    XMLWriter xw = new XMLWriter(fos);
  6. 写出文档
    xw.write(doc);
  7. 释放资源
    xw.close();

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//1. 通过文档帮助器, 创建空的文档对象
Document doc = DocumentHelper.createDocument();
//2. 向文档对象中, 加入根节点对象
Element books = doc.addElement("books");
//3. 向根节点中 丰富子节点
for(int i=0;i<1000;i++) {
//向根节点中加入1000个book节点.
Element book = books.addElement("book");
//向book节点, 加入id属性
book.addAttribute("id", 1+i+"");
//向book节点中加入name和info节点
Element name = book.addElement("name");
Element info = book.addElement("info");
name.setText("苹果"+i);
info.setText("哈哈哈"+i);
}
//4. 创建文件的输出流
FileOutputStream fos = new FileOutputStream("c:\\books.xml");
//5. 将文件输出流 , 转换为XML文档输出流
XMLWriter xw = new XMLWriter(fos);
//6. 写出XML文档
xw.write(doc);
//7. 释放资源
xw.close();
System.out.println("代码执行完毕");

11.4 XStream(对象->字符串)

快速的将Java中的对象, 转换为 XML字符串

使用步骤:

  1. 创建XStream 对象
    XStream x = new XStream();
  2. 修改类生成的节点名称 (默认节点名称为 包名.类名)
    x.alias("节点名称",类名.class);
  3. 传入对象 , 生成XML字符串
    String xml字符串 = x.toXML(对象);

案例:

1
2
3
4
5
Person p = new Person(1001, "张三", "不详");
XStream x = new XStream();
x.alias("haha", Person.class);
String xml = x.toXML(p);
System.out.println(xml);

11.5 JSON

概念:JavaScript Object Notation JS对象简谱 , 是一种轻量级的数据交换格式.

11.5.1 JSON格式

回想xml的对象格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//java
class Book{
private String name;
private String info;
get/set...
}
Book b = new Book();
b.setName(“金苹果”);
b.setInfo(“种苹果”);
...
//js:
var b = new Object();
b.name = "金苹果";
b.info = "种苹果";
//XML:
<book>
<name>金苹果</name>
<info>种苹果</info>
</book>
JSON:
{
"name":"金苹果",
"info":"种苹果"
}

格式:

  • 键与值之间使用冒号连接, 多个键值对之间使用逗号分隔.
  • 键值对的键 应使用引号引住 (通常Java解析时, 键不使用引号会报错. 而JS能正确解析.)
  • 键值对的值, 可以是JS中的任意类型的数据

数组格式:

  • 在JSON格式中可以与对象互相嵌套
    [JSON元素1, JSON元素2…]
1
2
3
4
5
6
7
8
9
10
11
12
{
"name":"伟杰老师",
"age":18,
"pengyou":["张三","李四","王二","麻子",{
"name":"野马老师",
"info":"像匹野马一样狂奔在技术钻研的道路上"
}],
"heihei":{
"name":"大长刀",
"length":"40m"
}
}

11.6 JSON解析

两个问题:

将Java中的对象 快速的转换为 JSON格式的字符串;将JSON格式的字符串, 转换为Java的对象。

11.6.1 Gson

  • 将对象转换为JSON字符串

    1. 引入JAR包
    2. 在需要转换JSON字符串的位置编写如下代码即可:
      String json = new Gson().toJSON(要转换的对象);

    案例:

    1
    2
    3
    Book b = BookDao.find();
    String json = new Gson().toJson(b);
    System.out.println(json);
  • 将JSON字符串转换为对象

    1. 引入JAR包
    2. 在需要转换Java对象的位置, 编写如下代码:
      对象 = new Gson().fromJson(JSON字符串,对象类型.class);
    1
    2
    3
    4
    String json = "{\"id\":1,\"name\":\"金苹果\",\"author\":\"李伟杰
    \",\"info\":\"嘿嘿嘿嘿嘿嘿\",\"price\":198.0}";
    Book book = new Gson().fromJson(json, Book.class);
    System.out.println(book);

    注意:传入的json字符串的格式!!!大括号和转义字符

11.6.2 FastJson

  • 将对象转换为JSON字符串

    1. 引入JAR包
    2. 在需要转换JSON字符串的位置编写如下代码即可:
      String json=JSON.toJSONString(要转换的对象);

    案例:

    1
    2
    3
    Book b = BookDao.find();
    String json = new Gson().toJson(b);
    System.out.println(json);
  • 将JSON字符串转换为对象

    1. 引入JAR包
    2. 在需要转换Java对象的位置, 编写如下代码:
      类型 对象名=JSON.parseObject(JSON字符串, 类型.class);

      List<类型> list=JSON.parseArray(JSON字符串,类型.class);
    1
    2
    3
    4
    String json = "{\"id\":1,\"name\":\"金苹果\",\"author\":\"李伟杰
    \",\"info\":\"嘿嘿嘿嘿嘿嘿\",\"price\":198.0}";
    Book book = new Gson().fromJson(json, Book.class);
    System.out.println(book);

    注意:传入的json字符串的格式!!!大括号和转义字符

12. 枚举、注解 、反射

12.1 枚举

12.1.1 概念和格式

  • 枚举的作用:

    JDK1.5引入了新的类型——枚举。在JDK1.5 之前,我们定义常量都是: public static fianl…. 。很难管理。

    枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。用于定义有限数量的一组同类常量,例如:

    1
    2
    3
    4
    5
    6
    错误级别:
    低、中、高、急
    一年的四季:
    春、夏、秋、冬
    商品的类型:
    美妆、手机、电脑、男装、女装...

    在枚举类型中定义的常量是该枚举类型的实例

  • 格式:

    1
    2
    3
    权限修饰符 enum 枚举名称 {
    实例1,实例2,实例3,实例4;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public enum Level {
    LOW(30), MEDIUM(15), HIGH(7), URGENT(1);
    private int levelValue;
    private Level(int levelValue) {
    this.levelValue = levelValue;
    }
    public int getLevelValue() {
    return levelValue;
    }
    }

12.1.2 常见方法

1
2
3
4
5
6
int compareTo(E o) //将此枚举与指定的订单对象进行比较。 
boolean equals(Object other) //如果指定的对象等于此枚举常量,则返回true。
String name() //返回此枚举常量的名称,与其枚举声明中声明的完全相同
int ordinal() //返回此枚举常量的序数(它在枚举声明中的位置,其中初始常量的序数为零)。
String toString() //返回声明中包含的此枚举常量的名称。
static <T extends Enum<T>>T valueOf(类<T> enumType, String name) //返回具有指定名称的指定枚举类型的枚举常量。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo1 {
public static void main(String[] args) {
System.out.println(Level.LOW.compareTo(Level.HIGH));
System.out.println(Level.LOW.compareTo(Level.MEDIUM));
System.out.println(Level.HIGH.compareTo(Level.LOW));
System.out.println(Level.HIGH.name());
System.out.println(Level.HIGH.toString());
System.out.println(Level.HIGH.ordinal());
System.out.println(Level.LOW.ordinal());
Level x = Enum.valueOf(Level.class, "HIGH");
System.out.println(x.name());
}
}

打印结果:

1
2
3
4
5
6
7
8
-2
-1
2
HIGH
HIGH
2
0
HIGH

12.1.3 枚举接口

所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。

每个枚举对象,都可以实现自己的抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Lshow{
void show();
}
//1.所有枚举对象实现相同方法
public enum Level implements Lshow{
LOW,MEDIUM,HIGH;
//给所有枚举对象统一添加的方法
public void show(){
System.out.println("级别对象调用的show方法");
}
}
//2.给每个枚举对象的继承的方法可以实现不同功能
public enum Level implements Lshow{
LOW{
public void show(){
System.out.println("低");
}
},MEDIUM{
public void show(){
System.out.println("中");
}
},HIGH{
public void show(){
System.out.println("高");
}
}
}

2方法中每个枚举类型的实例对象其实都是在通过内部类的方式进行实例化,上面因为是调用的无参构造方法将括号省略,在调用有参构造方法时括号又可以加上。

12.1.4 注意事项

  • 一旦定义了枚举,最好不要妄图修改里面的值,除非修改是必要的。
  • 枚举类默认继承的是java.lang.Enum类而不是Object类
  • 枚举类不能有子类,因为其枚举类默认被final修饰
  • 只能有private构造方法
  • switch中使用枚举时,直接使用常量名,不用携带类名、
  • 不能定义name属性,因为自带name属性
  • 不要为枚举类中的属性提供set方法,不符合枚举最初设计初衷。

12.2 注解-框架学习

框架中常用!

12.2.1 注解的简介和学习步骤

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。注释是给人看的,但注解是给机器看的,即可以把注释嵌入到代码执行流程中的机制。

Java 语言中的类、方法、变量、参数和包等都可以被标注。和注释不同,Java 标注可以通过反射获取标注内容。在编译器生成类文时,

标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

主要用于:

  • 编译格式检查
  • 反射中解析
  • 生成帮助文档
  • 跟踪代码依赖

学习步骤:

  • 概念
  • 怎么使用内置注解
  • 怎么自定义注解
  • 反射中怎么获取注解内容

12.2.2 内置注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SafeVarargs
Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

@Repeatable:标识某注解可以在同一个声明上使用多次
Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

@Override: //重写 *
定义在java.lang.Override

@Deprecated: //废弃 *
定义在java.lang.Deprecated
(为什么不删掉:后续发现问题但考虑到对之前版本的兼容性,一般不会修改代码,而是扩展代码)

@FunctionalInterface//函数式接口 *
Java 8 开始支持,标识一个匿名函数或函数式接口。

SuppressWarnings:抑制编译时的警告信息。 *
定义在java.lang.SuppressWarnings
三种使用方式:
1. @SuppressWarnings("unchecked") [^ 抑制单类型的警告]
2. @SuppressWarnings("unchecked","rawtypes") [^ 抑制多类型的警告]
3. @SuppressWarnings("all") [^ 抑制所有类型的警告]

抑制编译警告SuppressWarnings的注解的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
all:抑制所有警告
boxing:抑制装箱、拆箱操作时候的警告
cast:抑制映射相关的警告
dep-ann:抑制启用注释的警告
deprecation:抑制过期方法警告
fallthrough:抑制确在switch中缺失breaks的警告
finally:抑制finally模块没有返回的警告
hiding:抑制相对于隐藏变量的局部变量的警告
incomplete-switch:忽略没有完整的switch语句
nls:忽略非nls格式的字符
null:忽略对null的操作
rawtypes:使用generics时忽略没有指定相应的类型
restriction:抑制禁止使用劝阻或禁止引用的警告
serial:忽略在serializable类中没有声明serialVersionUID变量
static-access:抑制不正确的静态访问方式警告
synthetic-access:抑制子类没有按最优方法访问内部类的警告
unchecked:抑制没有进行类型检查操作的警告
unqualified-field-access:抑制没有权限访问的域的警告
unused:抑制没被使用过的代码的警告

12.2.3 元注解

作用:给其他注解加注解(对定义的注解进行配置)

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问

  • @Documented - 标记这些注解是否包含在用户文档中 javadoc。

  • @Target - 标记这个注解应该是哪种 Java 成员。

  • @Inherited - 标记这个注解是自动继承的

    1
    2
    3
    4
    1. 子类会继承父类使用的注解中被@Inherited修饰的注解
    2. 接口继承关系中,子接口不会继承父接口中的任何注解,不管父接口中使用的注解有没有
    @Inherited修饰
    3. 类实现接口时不会继承任何接口中定义的注解

12.2.4 自定义注解(实现Annotation接口)

image-20210325221443535

  • Annotation与RetentionPolicy 与ElementType 。每 1 个 Annotation 对象,都会有唯一的 RetentionPolicy 属性;至于 ElementType 属性,则有 1~n个

  • ElementType(注解的用途类型)
    “每 1 个 Annotation” 都与 “1~n 个 ElementType” 关联。当 Annotation 与某个 ElementType 关联时,就意味着:Annotation有了某种用途。例如,若一个 Annotation 对象是 METHOD 类型,则该Annotation 只能用来修饰方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package java.lang.annotation;
    public enum ElementType {
    TYPE, /* 类、接口(包括注释类型)或枚举声明 */
    FIELD, /* 字段声明(包括枚举常量) */
    METHOD, /* 方法声明 */
    PARAMETER, /* 参数声明 */
    CONSTRUCTOR, /* 构造方法声明 */
    LOCAL_VARIABLE, /* 局部变量声明 */
    ANNOTATION_TYPE, /* 注释类型声明 */
    PACKAGE /* 包声明 */
    }
  • RetentionPolicy(注解作用域策略)。

    “每 1 个 Annotation” 都与 “1 个 RetentionPolicy” 关联。

    • 若 Annotation 的类型为 SOURCE,则意味着:Annotation 仅存在于编译器处理期间,编译器处理完之后,该 Annotation 就没用了。 例如,” @Override” 标志就是一个 Annotation。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查!编译器处理完后,”@Override” 就没有任何作用了。

    • 若 Annotation 的类型为 CLASS,则意味着:编译器将 Annotation 存储于类对应的 .class 文件中,它是 Annotation 的默认为。

    • 若 Annotation 的类型为 RUNTIME,则意味着:编译器将 Annotation 存储于 class 文件中,并且可由JVM读入。

      1
      2
      3
      4
      5
      6
      7
      package java.lang.annotation;
      public enum RetentionPolicy {
      SOURCE, /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该
      Annotation信息了 */
      CLASS, /* 编译器将Annotation存储于类对应的.class文件中。默认行为 */
      RUNTIME /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
      }

格式

1
@interface 自定义注解名{}

上面的作用是定义一个 Annotation,我们可以在代码中通过 “@MyAnnotation1” 来使用它。@Documented, @Target, @Retention, @interface 都是来修饰 MyAnnotation1 的。含义:

  • @interface
    使用 @interface 定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是一个Annotation。定义 Annotation 时,@interface 是必须的。
    注意:它和我们通常的 implemented 实现接口的方法不同。Annotation 接口的实现细节都由编译器完成。通过 @interface 定义注解后,该注解不能继承其他的注解或接口。

  • @Documented
    类和方法的 Annotation 在缺省情况下是不出现在 javadoc 中的。如果使用 @Documented 修饰该Annotation,则表示它可以出现在 javadoc 中。

    定义 Annotation 时,@Documented 可有可无;若没有定义,则 Annotation 不会出现在 javadoc

  • @Target(ElementType.TYPE)

    前面我们说过,ElementType 是 Annotation 的类型属性。而 @Target 的作用,就是来指定Annotation 的类型属性。

    @Target(ElementType.TYPE) 的意思就是指定该 Annotation 的类型是 ElementType.TYPE。这就意味着,MyAnnotation1 是来修饰”类、接口(包括注释类型)或枚举声明”的注解。

    定义 Annotation 时,@Target 可有可无。若有 @Target,则该 Annotation 只能用于它所指定的地方;若没有 @Target,则该 Annotation 可以用于任何地方。

  • @Retention(RetentionPolicy.RUNTIME)
    前面我们说过,RetentionPolicy 是 Annotation 的策略属性,而 @Retention 的作用,就是指定Annotation 的策略属性。
    例如@Retention(RetentionPolicy.RUNTIME) 的意思就是指定该 Annotation 的策略是RetentionPolicy.RUNTIME。这就意味着,编译器会将该 Annotation 信息保留在 .class 文件中,并且能被虚拟机读取。
    定义 Annotation 时,@Retention 可有可无。若没有 @Retention,则默认是RetentionPolicy.CLASS

注意事项

  1. 定义的注解,自动继承了java.lang,annotation.Annotation接口
  2. 注解中的每一个方法,实际是声明的注解配置参数
    方法的名称就是 配置参数的名称
    方法的返回值类型,就是配置参数的类型。只能是:基本类型/Class/String/enum
  3. 可以通过default来声明参数的默认值
  4. 如果只有一个参数成员,一般参数名为value
  5. 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。

总结

元注解和自定义注解相辅相成,元注解可以对自定义注解加以修饰,修饰的方法就是给元注解传入一些枚举常量

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@MyAnnotation(value = "张三", num = 122)
public class Demo1 {
public static void main(String[] args) {

}
}
@Documented
//用途类型
@Target({ElementType.METHOD,ElementType.TYPE})
//可以继承
@Inherited
//保存策略
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation{
//本质上也是抽象方法
String value() default "xiaoming";//不加default则调用注解时相当于创建注解对象,必须传入参数,加入默认值即可以不用传值
int num();
}

12.3 反射-优化代码

1
2
JAVA反射机制是在运行状态中,获取任意一个类的结构 , 创建对象 , 得到方法,执行方法 , 属性 !;
这种在运行状态动态获取信息以及动态调用对象方法的功能被称为java语言的反射机制

12.3.1 类加载器

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。

java默认有三种类加载器,BootstrapClassLoader、ExtensionClassLoader、AppClassLoader。

  1. BootstrapClassLoader(引导启动类加载器):

    嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库,引导启动类加载器无法被应用程序直接使用。

  2. ExtensionClassLoader(扩展类加载器):
    ExtensionClassLoader是用JAVA编写,且它的父类加载器是Bootstrap。是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。它的父加载器是BootstrapClassLoader

  3. App ClassLoader(应用类加载器):
    App ClassLoader是应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件。它的父加载器为Ext ClassLoader

image-20210326101221084

多个类加载器如何避免类的重复加载?

类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。学习类加载器时,掌握Java的委派概念很重要。

双亲委派模型:如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。

12.3.2 加载配置文件

类加载器的使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo3 {
public static void main(String[] args) throws IOException {
//通过字节码文件获取类加载器
ClassLoader cl = Demo3.class.getClassLoader();
//打印结果:jdk.internal.loader.ClassLoaders$AppClassLoader@1f89ab83
System.out.println(cl);
//config.txt文件必须在src路径下才能获取,否则报空指针异常
InputStream is = cl.getResourceAsStream("config.txt");
BufferedReader bf = new BufferedReader(new InputStreamReader(is));
String s = bf.readLine();
System.out.println(s);
bf.close();
}
}

给项目添加resource root目录

image-20210326103308012

注意:通过类加载器加载资源文件默认加载的是src路径下的文件,但是当项目存在resource root目录时,就变为了加载resource root下的文件了

image-20210326103439658

12.3.3 Class和加载方式

所有类型的Class对象

要想了解一个类,必须先要获取到该类的字节码文件对象。在Java中,每一个字节码文件,被加载到内存后,都存在一个对应的Class类型的对象。

image-20210326104315107

类编译成的class文件,在加载到内存中以后,同样是作为一个对象存在,同样有方法获取类的实例化对象

图中的Class<T>就表示类 类型。

得到Class对象的几种方式

  • 包名.类名.class得到一个类的 类对象

    前提:如果在编写代码时, 知道类的名称, 且类已经存在

  • (Class<T>)Class.forName(包名+类名)

    次方法可以用于类尚不存在,可以将类名作为参数传入

  • (Class<T>)对象.getClass()

    前提:如果拥有类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Demo4 {
public static void main(String[] args) throws ClassNotFoundException {
//1.包名.类名.class
Class<Person> p1 = com.java.demo4.Person.class;
System.out.println(p1);

//2.Class 对象.getClass()
Person p = new Person();
Class<Person> p2 = (Class<Person>) p.getClass();
System.out.println(p2);

//3.Class.forName(包名+类名)
Class<Person> p3 = (Class<Person>) Class.forName("com.java.demo4.Person");
System.out.println(p3);
System.out.println(p1 == p2);
System.out.println(p2 == p3);
}
}
//打印结果
class com.java.demo4.Person
class com.java.demo4.Person
class com.java.demo4.Person
true
true

上述的三种方式, 在调用时, 如果类在内存中不存在, 则会加载到内存 ! 如果类已经在内存中存在, 不会重复加载, 而是重复利用

特殊的类对象

1
2
3
4
5
基本数据类型的类对象:
基本数据类型.clss
包装类.type
基本数据类型包装类对象:
包装类.class

12.3.4 反射中的构造方法

问题在于,如何从类对象中提取构造方法,如何利用构造方法创建一个对象。

提取构造方法

  • 通过指定的参数类型, 获取指定的单个构造方法:getConstructor(参数类型的class对象数组)
  • 获取构造方法数组:getConstructors()

  • 获取所有权限的单个构造方法:getDeclaredConstructor(参数类型的class对象数组)

  • 获取所有权限的构造方法数组:getDeclaredConstructors()

这里的所有权限指除了public以外的权限

创建一个对象

常用方法:newInstance(Object... para)
调用这个构造方法, 把对应的对象创建出来
参数: 是一个Object类型可变参数, 传递的参数顺序 必须匹配构造方法中形式参数列表的顺序!

如何越过私有构造方法的限制

setAccessible(boolean flag)
如果flag为true 则表示忽略访问权限检查 !(可以访问任何权限的方法

实际案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.反射获取无参构造方法
Class<Person> pClass = (Class<Person>) Class.forName("com.java.demo4.Person");
//找到无参构造方法
Constructor<Person> c1 = pClass.getConstructor();
//通过无参构造方法创建对象
Person p = c1.newInstance();
System.out.println(p);

//2.反射获取有参构造方法,传递的参数类型是Class对象数组
Constructor<Person> c2 = pClass.getConstructor(String.class,int.class);
Person p1= c2.newInstance("张三", 20);
System.out.println(p1);

//3.反射获取私有权限的构造方法
Constructor<Person> c3 = pClass.getDeclaredConstructor(String.class);
//设置忽略权限检查
c3.setAccessible(true);
Person p2= c3.newInstance("张三");
System.out.println(p2);
}
//打印结果
Person{name='null', age=0}
Person{name='张三', age=20}
Person{name='张三', age=0}

12.3.5 反射获取方法

通过Class对象获取,与Constructor的获取方法类似,执行方法需要先 通过反射构造出实例变量

  1. getMethod(String methodName , class.. clss)
    根据参数列表的类型和方法名, 得到一个方法(public修饰的)
  2. getMethods()
    得到一个类的所有方法 (public修饰的)
  3. getDeclaredMethod(String methodName , class.. clss)
    根据参数列表的类型和方法名, 得到一个方法(除继承以外所有的:包含私有, 共有, 保护, 默认)
  4. getDeclaredMethods()
    得到一个类的所有方法 (除继承以外所有的:包含私有, 共有, 保护, 默认)

执行获取到的方法:

  • invoke(Object o,Object... para)

    执行o中的这个方法,传入相应参数

  • getName()

    获取方法的方法名称

  • setAccessible(boolean flag)

    如果flag为true 则表示忽略访问权限检查 !(可以访问任何权限的方法)

实例:

1
2
3
4
5
6
7
8
9
10
11
    public static void main(String[] args) throws Exception {
Class c1 = Class.forName("com.java.demo4.Person");
Constructor ct = c1.getConstructor();
Object o = ct.newInstance();
Method setName = c1.getMethod("setName", String.class);
//参数1. 要调用方法的对象
//参数2. 要传递的参数列表
setName.invoke(o, "张三");
System.out.println(o);
}
//

12.3.6 反射获取属性

通过Class对象获取,与Constructor、Method的获取方法类似,执行方法需要先通过反射构造出实例变量

  1. getDeclaredField(String filedName)
    根据属性的名称, 获取一个属性对象 (所有属性)
  2. getDeclaredFields()
    获取所有属性
  3. getField(String filedName)
    根据属性的名称, 获取一个属性对象 (public属性)
  4. getFields()
    获取所有属性 (public)

获取或设置对象中属性

  1. get(Object o)
    参数: 要获取属性的对象

  2. set(Object o , Object value)

    参数1. 要设置属性值的 对象
    参数2. 要设置的值

  3. getName()
    获取属性的名称

  4. setAccessible(boolean flag)
    如果flag为true 则表示忽略访问权限检查 !(可以访问任何权限的属性)

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
Class c = Class.forName("com.java.demo4.Person");
Constructor ct = c.getConstructor();
Object o = ct.newInstance();
Field phoneNum = c.getField("phoneNumber");
phoneNum.set(o,123456);
System.out.println(o);
}

12.3.7 反射获取注解

回一下注解部分的内容:自定义和添加注解?

  • 自定义注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//新建class,选择Annotation类型,对新建的Annotation进行修饰
//MyAnnotationTable修饰类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface MyAnnontationTable {
//用于标注类对应的表格名称
String value();
}

//ColumnAnnotation用于修饰属性
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface ColumnAnnotation {
String columName();
String type();
String length();
}
  • 添加注解
1
2
3
4
5
6
7
8
9
//在类声明前添加类的注解,在成员属性前添加属性注解
@MyAnnontationTable("person_test")
public class Person {
@ColumnAnnotation(columName = "name",type = "String",length = "10")
private String name;
@ColumnAnnotation(columName = "age",type = "String",length = "11")
private int age;
@ColumnAnnotation(columName = "phoneNumber",type = "String",length = "12")
public int phoneNumber;

可以看到注解中其实有我们想要的信息,向类注解中的信息可以作为数据库中的表头,属性注解中的内容可以作为数据库中的属性

接下来的问题就是如何通过反射获取注解

反射获取注解

通过Class对象获取,与Constructor、Method、Field的获取方法类似,执行方法需要先通过反射构造出实例变量

  • 获取类的单个或全部注解

    注解类型 对象名 = (注解类型) c.getAnnotation(注解类型.class)

    注解类型 对象名 = (注解类型) c.getAnnotations(注解类型.class)

  • 获取属性的注解

    先通过反射获取属性对象,再通过属性对象调用上面的获取注解方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public static void main(String[] args) throws Exception {
Class c = Class.forName("com.java.demo4.Person");
Annotation[] an = c.getAnnotations();
//获取类的全部注解(此时只能获取一个类的注解)
for(Annotation a: an){
System.out.println(a);
}
//获取类的注解,因为只有一个,可以用注解类型.class获取
MyAnnontationTable mat = (MyAnnontationTable)c.getAnnotation(MyAnnontationTable.class);
System.out.println(mat);

//获取全部的属性注解
Field[] fs = c.getDeclaredFields();
for(Field f: fs){
ColumnAnnotation ca = (ColumnAnnotation)f.getAnnotation(ColumnAnnotation.class);
System.out.println(f.getName()+"\t"+ca.columName()+"\t"+ca.length()+"\t"+ca.type());
}
}
//打印结果

12.4 内省

12.4.1 概念

基于反射 , java所提供的一套应用到JavaBean的API

什么是bean类?

  • 一个定义在包中的类 ,
  • 拥有无参构造器
  • 所有属性私有,
  • 所有属性提供get/set方法
  • 实现了序列化接口

12.4.2 Introspector

先说目的:获取所有属性的get和set方法

image-20210329145426502

其中getPropertyDescription()方法获取的PropertyDescription类型的数组,其中的每个PropertyDescription对象都是一个属性的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    public static void main(String[] args) throws IntrospectionException {
Class c = Express.class;
BeanInfo bi = Introspector.getBeanInfo(c);
//获取get和set方法的对象
PropertyDescriptor[] pds = bi.getPropertyDescriptors();
//将get和set方法从对象中抽离
for(PropertyDescriptor pd: pds){
Method get = pd.getReadMethod();
Method set = pd.getWriteMethod();
System.out.println(get);
System.out.println(set);
System.out.println(pd.getName());
System.out.println(pd.getPropertyType());
}
}
//打印结果
public java.lang.String com.java.demo.Express.getAddress()
public void com.java.demo.Express.setAddress(java.lang.String)
address
class java.lang.String
public final native java.lang.Class java.lang.Object.getClass()
null
class
class java.lang.Class
public java.lang.String com.java.demo.Express.getName()
public void com.java.demo.Express.setName(java.lang.String)
name
class java.lang.String
public java.lang.String com.java.demo.Express.getNumber()
public void com.java.demo.Express.setNumber(java.lang.String)
number
class java.lang.String
public java.lang.String com.java.demo.Express.getPhoneNumber()
public void com.java.demo.Express.setPhoneNumber(java.lang.String)
phoneNumber
class java.lang.String

Process finished with exit code 0

注意:

  • 虽然这个工具不一定用的上,但是在开发的时候bean类的编写一定要规范!!!比如属性没有get和set方法,通过内省机制是获取不到的
  • 对于boolean类型的变量boolean flag,在调用getWriteMethod()时,结果不是getFlag(),而是isFlag()