文章目录

  • 一、基础语法
  • 1.1 环境变量配置
  • 1.2 JDK、JRE、JVM
  • 1.3 基本数据类型
  • 1.4 运算符
  • 1.5 保留字
  • 二、面向对象
  • 2.1 数组
  • 2.2 字符串
  • 2.3 构造器
  • 2.4 继承与多态
  • 2.5 super 和 this
  • 2.6 == 和 equals()
  • 2.7 重写和重载
  • 2.8 可变参数
  • 2.9 类字段与类方法
  • 2.10 代码块和类加载
  • 2.11 单例设计模式
  • 2.12 final
  • 2.13 抽象类
  • 2.14 接口
  • 2.15 内部类
  • 三、枚举与注解
  • 3.1 自定义枚举类
  • 3.2 enum 枚举类
  • 3.3 Enum 类成员方法
  • 3.4 注解介绍
  • 3.5 三个常用注解
  • 3.6 四个元注解
  • 四、异常与泛型
  • 4.1 异常体系图
  • 4.2 try-catch-finally-return
  • 4.3 自定义异常
  • 4.4 throws 和 throw
  • 4.5 泛型介绍
  • 4.6 泛型类型
  • 4.7 自定义泛型
  • 五、常用类
  • 5.1 包装类
  • 5.2 String
  • 5.3 StringBuffer
  • 5.4 Arrays
  • 5.5 BigDecimal
  • 5.6 日期类
  • 六、集合类
  • 6.1 Collection
  • 6.2 Iterator
  • 6.3 List
  • 6.4 Map
  • 6.5 Set
  • 6.6 集合遍历
  • 6.7 集合选择
  • 6.8 Collections
  • 七、多线程
  • 7.1 线程概述
  • 7.2 线程创建
  • 7.3 start 与 run
  • 7.4 Thread
  • 7.5 线程插队与礼让
  • 7.6 守护线程
  • 7.7 线程的七大状态
  • 7.8 线程同步机制
  • 7.9 线程死锁
  • 7.10 多线程卖票
  • 八、IO
  • 8.1 File
  • 8.2 IO 流的分类
  • 8.3 字节流与字符流
  • 8.4 节点流与处理流
  • 8.5 对象处理流
  • 8.6 标准输入输出流
  • 8.7 转换流
  • 8.8 Properties
  • 九、网络编程
  • 9.1 IP 与域名
  • 9.2 端口与协议
  • 9.3 InetAddres
  • 9.4 TCP、UDP 与 Socket
  • 9.5 TCP 编程
  • 9.6 UDP 编程
  • 9.7 netstat
  • 十、反射
  • 10.1 反射机制
  • 10.2 程序三阶段
  • 10.3 Class
  • 10.4 类加载
  • 10.5 反射爆破
  • 十一、JDBC
  • 11.1 JDBC 原理
  • 11.2 五种连接方式
  • 11.3 ResultSet
  • 11.4 SQL 注入
  • 11.5 PreparedStatement
  • 11.6 JDBC 事务操作
  • 11.7 批处理
  • 11.8 数据库连接池
  • 11.9 DBUtils


一、基础语法

1.1 环境变量配置

  1. Windows 环境变量配置:
  • JAVA_HOME = D:\Java\jdk1.8.0_131
  • Path = %JAVA_HOME%\bin
  • CLASSPATH = .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
  1. Linux 环境变量配置:
  • export JAVA_HOME=/usr/local/java/jdk1.8.0_311
  • export PATH=$JAVA_HOME/bin:$PATH

1.2 JDK、JRE、JVM

  1. JDK = JRE + Java 开发工具(java、javac、javap、javadoc ······)
  2. JRE = JVM + Java 核心类库(dt.jar、tools.jar)
  3. JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理和寄存器,包含在 JRE 中

java的stu java的study_Java

1.3 基本数据类型

  1. Java 数据类型中 没有无符号类型
  2. Java 浮点类型默认为 double 类型,定义 float 类型时需要在末尾加上 F 或 f,如 3.14F
  • 浮点数组成:符号位 + 指数位 + 尾数位
  1. Java 对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要 1 bit,但通常 JVM 内部会把 boolean 表示为 4 字节 整数
  2. 八大基本数据类型的数据范围:最高位用作符号位

类型

字节数

范围

byte

1

[-27, 27-1] => [-128, 127]

boolean

1

true or false

char

2

ISO 单一字符

short

2

[-215, 215-1] => [-32768, 32767]

int

4

[-231, 231-1] = > [-2147483648, 2147483647]

float

4

-3.403E38 - 3.403E38

long

8

[-263, 263-1] => [-9223372036854774808, 9223372036854774807]

double

8

-1.798E308 - 1.798E308

  1. 自动类型转换:
  • byte 自动类型:byte -> short -> int -> long -> float -> double
  • char 自动类型:char -> int -> long -> float -> double(char 直转为 int
  • byte、char、short 在进行运算时,当作 int 处理
  1. 浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做 位运算和移位运算
  2. 整数运算在除数为 0 时会报错,而 浮点数 运算在除数为 0 时不会报错,但会返回以下几个特殊值(NaN:Not a Number、Infinity:无穷大、-Infinity:负无穷大)
// java.lang.ArithmeticException
int i = 10 / 0;
// j = Infinity
double j = 10.0 / 0;
  1. 如果强制类型转换后超过了整型能表示的最大范围,将返回整型的最大值
double d = Double.MAX_VALUE;
// max = Integer.MAX_VALUE = 2147483647
int max = (int) d;
  1. 如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型
double a = 1.2;
// 对浮点数四舍五入
a += 0.5;
int b = (int) a;

1.4 运算符

  1. 自增、自减运算符
int a = 1;
int b = 1;
// the value changed at 'a++' is never used
a = a++;
b = ++b;
// output: a = 1, b = 2
System.out.println("a = " + a + ", b = " + b);
int a = 3;
// b = 3 + 5
int b = a++ + ++a;
// output: a = 5, b = 8
System.out.println("a = " + a + ", b = " + b);
  1. 三元运算符
Object object = true ? new Integer(1) : new Double(2.0);
// 三元运算符需要看作一个整体,故输出 1.0;if-else 分支结构则输出 1
// output: 1.0
System.out.println(object);
  1. 位运算
  • 算术右移(>>):低位丢弃,符号位不变,高位补符号位
  • 算术左移(<<):符号位不变,低位补 0
  • 无符号右移(>>>):低位丢弃,符号位不变,高位补 0
  1. 运算符优先级:自上而下优先级依次降低

运算顺序

操作符

. () {} ; ,

R -> L

++ – ~ !

L -> R

* / %

L -> R

+ -

L -> R

<< >> >>>

L -> R

< > <= >= instanceof

L -> R

== !=

L -> R

&

L -> R

^

L -> R

|

L -> R

&&

L -> R

||

L -> R

? :

R -> L

= *= /= %=

R -> L

+= -= <<= >>=

R -> L

>>>= &= ^= |=

1.5 保留字

java的stu java的study_Java_02

  1. assert:断言
  2. const:预留关键字
  3. goto:预留关键字
  4. transient:使用 transient 修饰的字段在对象序列化时该字段不会被序列化
  5. strictfp:strict float point (精确浮点) ,strictfp 关键字可应用于类、接口、方法。使用 strictfp 关键字声明一个方法时,该方法中所有的 float 和 double 表达式都严格遵守 FP-strict 的限制,符合 IEEE-754 规范
strictfp double add(double a, double b) {
    return a + b;
}
  1. volatile:JVM 提供的轻量级同步机制。作用是:保证可见性、禁止指令重排、不保证原子性
  2. switch:switch 的 case 后跟常量或常量表达式,switch 中的表达式只能是以下类型中的一种:byte、short、char、int、enum、String

二、面向对象

2.1 数组

  1. 静态初始化方式创建数组
int[] arr = new int[]{1, 2, 3};
int[][] matrix = new int[][]{{1, 2}, {3, 4, 5}, {6, 7, 8, 9}};
// output:[[1, 2], [3, 4, 5], [6, 7, 8, 9]]
System.out.println(Arrays.deepToString(matrix));
  1. 数组默认值:new 方式创建数组,若没有赋值则有默认值,由此可看出数组是引用类型
float[] arr = new float[5];
// output: [0.0, 0.0, 0.0, 0.0, 0.0]
System.out.println(Arrays.toString(arr));

2.2 字符串

  1. Java 使用 Unicode 字符集,所以一个英文字符和一个中文字符都用一个 char 类型表示,占用 2 字节
  2. 常用字符集占用的字节数:

字符集

字符占用字节数

汉字占用字节数

ASCII

1

不支持中文

Unicode

2

2

UTF - 8

1

3

GBK

1

2

2.3 构造器

  1. 调用构造器时堆中已经分配了对象的空间,构造器只负责对象的初始化,并不创建对象
  2. 字段默认值:类的属性有默认值,局部变量没有默认值(数组除外
  3. 对象创建流程:
  • 加载类信息,生成 .class 对象
  • 在堆中为对象分配空间
  • 栈:一般存放基本数据类型(局部变量)
  • 堆:存放对象(数组)
  • 方法区:常量池(字符串)、类加载信息
  • 完成对象的默认初始化
  • 构造器完成对象的初始化

2.4 继承与多态

  1. 子类继承了父类所有的方法和属性,但子类不能直接访问父类的私有属性
  2. 当创建子类的对象时,无论调用了子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类中使用 super(...) 显式地调用父类的构造器完成父类的初始化工作,否则编译通不过
  • super()this() 都只能放在 构造器的第一行,因此这两个方法不能同在一个构造器中
  • instanceof 实际上判断的是一个变量所指向的实例是否是指定类型,或者这个类型的子类型
  1. 多态:针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法(谓之动态绑定机制)

2.5 super 和 this

区别点

this

super

属性

访问本类属性,没有则逐层查找父类

查找父类属性

方法

访问本类方法,没有则逐层查找父类

查找父类方法

构造器

行首调用本类其它构造器

行首调用父类指定构造器

特殊点

表示当前对象

表示子类中访问父类的对象

  1. 不能使用 super 访问父类的 private 成员
  2. this 不能在类定义的外部使用,只能在类定义的方法中使用

2.6 == 和 equals()

作用

==

判断基本类型时判断值是否相等;判断引用类型时判断是否引用到同一个对象

equals()

只能判断引用类型是否引用到同一个对象,子类往往重写从而实现判断内容相等

2.7 重写和重载

  1. 重写:类实现接口或子类继承父类方法是发生重写
  • 重写时返回类型可以是父类或父类的子类
  • 重写时不能缩小父类方法的访问权限
  1. 重载:方法重载需保证方法签名不同(返回值不是方法签名的一部分)

2.8 可变参数

  1. 可变参数的 实参 可以是 0 个或是多个,其本质就是数组
  2. 方法的形参列表中可以同时有普通形参和一个可变参数,但必须保证可变参数是形参列表的最后一个参数
public double add(double a, int b, int... args) {
    double sum = a + b;
    for (int c : args) {
        sum += c;
    }
    return sum;
}

2.9 类字段与类方法

  1. 类变量也即静态变量,类方法也即静态方法
  2. JDK8 以前类变量放在 方法区的静态域 中,JDK8 以后放在 堆中类的 class 对象尾部
  3. 静态代码块、静态方法属于 而不属于类实例,只能调用静态成员;普通方法和普通代码块可以调用任何成员
  4. 当方法中不涉及到与任何对象相关的成员时,则可以将方法设计为静态方法以提高运行效率

2.10 代码块和类加载

  1. 代码块可以理解为只有方法体的方法,它没有方法名、返回值、形参列表,不能通过对象或类显式调用,只有在加载类时自动隐式调用
  2. 代码块的修饰符只能是 static 或无修饰符,块体中的语句可以是任何正确逻辑语句
  3. 如果只是调用类的静态成员,代码块并不会执行(类未加载)
public class Test {
    public static void main(String[] args) {
        // output: 2
        System.out.println(GirlFriend.AGE);
    }

}

class GirlFriend {
    public static final int AGE = 2;

    // 当使用到 GirlFriend.AGE 时,类未加载,代码块不会被执行
    static {
        System.out.println("I am your only girlfriend");
    }

    {
        System.out.println("I am your second girlfriend");
    }
}
  1. 类代码块的执行顺序优先于构造器
public class Test {
    public static void main(String[] args) {
        /*
         * output:
         * I am your only girlfriend
		 * I am your second girlfriend
		 * I am your third girlfriend, my name is name
         */
        
        new GirlFriend("Alice");
    }

}

class GirlFriend {
    public static final int AGE = 2;

    public GirlFriend(String name) {
        System.out.println("I am your third girlfriend, my name is " + name);
    }

    // 当使用到 GirlFriend.AGE 时,类未加载,代码块不会被执行
    static {
        System.out.println("I am your only girlfriend");
    }

    {
        System.out.println("I am your second girlfriend");
    }
}
  1. 类加载时机:创建该类的对象时、创建子类的对象时父类也会加载
  2. Java 代码执行顺序:
  • 父类的静态代码块和静态属性
  • 子类的静态代码块和静态属性
  • 父类的普通代码块与普通属性
  • 父类的构造器
  • 子类的普通代码块与普通属性
  • 子类的构造器

2.11 单例设计模式

  1. 设计模式:在大量的实践中总结和理论化后优选的代码结构、编程风格以及解决问题的思考方式。设计模式好比经典的棋谱,不同的棋局采用不同的棋谱,免去再次思考和摸索的过程
  2. 类的单例设计模式:采取一定的方法使得在整个软件系统中,某个类有且仅有一个对象实例,并且该类只提供一个取得该对象的方法
  3. 单例设计模式分为饿汉式和懒汉式:
  • 饿汉式在类加载时就创建实例,不存在线程安全问题
  • 懒汉式在使用到对象时才创建,存在线程安全问题
  1. 单例模式的实现:
  • 构造器私有化
  • 在类的内部创建对象
  • 向外部提供一个静态的公共方法取得该对象
/**
 * 单例设计模式:饿汉式
 */
class GirlFriend {
    private GirlFriend() {
    }

    private static final GirlFriend girlFriend = new GirlFriend();

    public static GirlFriend getGirlFriend() {
        return girlFriend;
    }
}

/**
 * 单例设计模式:懒汉式
 */
class BoyFriend {
    private BoyFriend() {
    }

    private static BoyFriend boyFriend;

    public static BoyFriend getBoyFriend() {
        if (boyFriend == null) {
            boyFriend = new BoyFriend();
        }
        return boyFriend;
    }
}
  • java.lang.Runtime 类就是经典的懒汉式单例模式
public class Runtime {
   private static Runtime currentRuntime = new Runtime();

   public static Runtime getRuntime() {
       return currentRuntime;
   }

   private Runtime() {}
}

2.12 final

  1. final 的使用场景:
  • 不希望某个类被继承
  • 不希望父类的方法被子类重写
  • 不希望类的成员值被修改
  • 不希望局部变量的值被修改
  1. final 修饰的属性必须初始化,初始化后不能更改。初始化可在三个地方进行:
  • 定义时初始化
  • 构造器中初始化
  • 代码块中初始化
public static final int MAX = 9;
public static final int MIN;

static {
    MIN = -1;
}
  1. 如果非 final 类中含有 final 修饰的方法,则该方法可以被继承但不能被重写
  2. final 和 static 搭配使用效率更高,因为底层编译器做了优化处理,不会导致类加载

2.13 抽象类

  1. 当父类的某些方法需要被声明,但又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类
  2. 有抽象方法的类一定是抽象类,抽象类不一定有抽象方法,abstract 只可以修饰类和方法。抽象方法没有方法体
public abstract class Node {
    public int getMax() {
        return 100;
    }
}
  1. 如果一个类继承自抽象类,则必须实现父类的所有抽象方法,除非它也是一个抽象类
  2. 抽象方法不能使用 private、final、static 来修饰,因为这些修饰符与重写相违背

2.14 接口

  1. JDK8 之前的版本,接口中所有的方法都没有方法体即全是抽象方法。JDK8 及之后的版本,接口中可以有静态字段(默认是 public static final)、默认方法
interface Study {
    /*
     * 类实现接口时拥有此默认方法
     */
    default void english() {
        System.out.println("Study english");
    }

    static void math() {
        System.out.println("Study math");
    }
}
  1. 抽象类实现接口可以不用实现方法、接口继承接口时可以不用实现方法
  2. 接口对比:
  • Java 的接口特指 interface 的定义,表示一个接口类型和一组方法签名
  • 编程接口泛指接口规范,如方法签名,数据格式,网络协议等
  1. 继承与接口对比:
  • 继承解决代码的复用性与可维护性
  • 接口用来设计规范方法,一定程度上实现代码解耦

2.15 内部类

  1. 局部内部类:定义在外部类的局部位置且有类名,比如方法或代码块中。具有以下特点:
  • 局部内部类可以理解为方法的局部变量,不可以添加访问修饰符,但可以添加 final
  • 内部类可以直接访问外部类的所有成员,包括私有成员
  • 如果外部类和局部内部类的成员重名时,默认遵循就近原则。若想访问外部类的成员,可以使用 外部类名.this.成员 的方式去访问
class Outer {
   private String name = "Spring-_-Bear";
   public void m1() {
       class Inner {
           private String name = "springbear";
           private void test() {
               // 调用局部内部类的成员
               System.out.println(name);
               // 调用外部类的成员
               System.out.println(Outer.this.name);
           }
       }
       // 调用局部内部类的方法
       new Inner().test();
   }
}
  1. 匿名内部类:定义在外部类的局部位置且没有类名,比如方法或代码块。好比一个没有名字的局部变量。经典使用场景是在方法参数位置直接实现接口当作实参传递,简洁高效不冗余
  • 匿名内部类本身既是一个类的定义同时也是一个对象,因而从语法层面看,它既有类的特征也有对象的特征
public class Anonymous {
    public static void main(String[] args) {
        Cry cry = new Cry() {
            @Override
            public void cry() {
                System.out.println("cry~~~");
            }
        };
        cry.cry();
        System.out.println(cry.getClass());


        new Cry() {
            @Override
            public void cry() {
                System.out.println("cry~~~");
            }
        }.cry();
    }
}

interface Cry {
    void cry();
}
  1. 成员内部类:成员内部类可以理解为类的成员
public class C02 {
   public static void main(String[] args) {
       new Outer().new Inner().m();
   }
}

class Outer {
   private String name = "Spring-_-Bear";
   // 成员内部类
   public class Inner {
       public void m() {
           System.out.println(name);
       }
   }
}
  1. 静态内部类:静态内部类可以理解类的静态成员,可以直接访问外部类的所有静态成员,但不能访问非静态成员

三、枚举与注解

3.1 自定义枚举类

  1. 枚举:枚举是一组常量的集合,属于一种特殊的类,里面只包含一组有限的特定对象
  2. 自定义枚举类:
  • 构造器私有化
  • 不提供 setXxx() 方法
  • 对枚举属性使用 statc + final 修饰以实现底层优化
class Season {
    private String name;
    private String description;

    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season SUMMER = new Season("夏天", "炎热");
    public static final Season AUTUMN = new Season("秋天", "凉爽");
    public static final Season WINTER = new Season("冬天", "寒冷");

    private Season(String name, String description) {
        this.name = name;
        this.description = description;
    }

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

3.2 enum 枚举类

enum 修饰的类默认继承 Enum 类并且是一个 final 类,故不能继承其它类,但可以实现接口

enum Season {
    /**
     * 必须位于行首且以逗号间隔,分号结尾。如果使用的是无参构造器创建枚举对象,则括号可以省略
     */
    SPRING("春天", "温暖"),
    SUMMER("夏天", "炎热"),
    AUTUMN("秋天", "凉爽"),
    WINTER("冬天", "寒冷");

    private final String name;
    private final String description;

    Season(String name, String description) {
        this.name = name;
        this.description = description;
    }

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

java的stu java的study_Socket_03

3.3 Enum 类成员方法

方法

功能

toString()

返回当前对象名,子类可重写

name()

返回当前对象常量名,子类不可重写

ordinal()

返回当前对象的索引号,默认从 0 开始

values()

返回当前枚举类中的所有常量

valueOf()

将所给字符串包装为枚举对象,若该枚举对象不存在则抛异常

compareTo()

比较两个枚举常量的索引号

3.4 注解介绍

  1. 注解(Annotation)也称为元数据(Metadata),用于修饰解释包、类、方法、属性、构造器、局部变量等数据信息。与注释一样,注解不影响程序逻辑,但可以被编译或运行,相当于嵌入在代码中的补充信息
  2. 在 Java SE 中,注解的使用目的比较简单,例如标记过时的方法、忽略警告等。但注解在 Java EE 中注解占据重要角色,例如用来配置应用程序的任何切面、代替 Java EE 旧版中所遗留的冗杂代码和 XML 配置等

3.5 三个常用注解

  1. @Override:重写方法时如果添加了 @Override 注解,则编译器就会检查是否真的重写了父类的方法,如果未重写则编译报错
// @Target(ElementType.METHOD) 是修饰注解的注解,称为元注解,用于说明此注解作用位置
@Target(ElementType.METHOD)
// @Retention(RetentionPolicy.SOURCE) 也是元注解,说明此注解的作用域(源码、运行时···)
@Retention(RetentionPolicy.SOURCE)
// @interface 并不指此类是接口,而是说明此类是注解类,在 JDK1.5 之后加入
public @interface Override {
}
  1. @Deprecated:标记该方法已过时
  2. @SupressWarning:抑制编译器警告

3.6 四个元注解

  1. @Retention:指定注解的作用域,如 SOURCE、CLASS、RUNTIME
  2. @Target:指定注解的使用位置,如 TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
  3. @Documented:指定注解在 javadoc 中体现,使用 @Documented 元注解的注解 @Retention 必须为 RUNTIME
  4. @Inherited:指定子类可以继承父类的注解

四、异常与泛型

4.1 异常体系图

java的stu java的study_Java_04

  1. Error(错误):Java 虚拟机无法解决的严重问题,如 JVM 系统内部错误、资源耗尽等严重情况
  2. Exception(异常):其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。分为编译时异常和运行时异常

4.2 try-catch-finally-return

  1. 如果出现异常,则 try 块中发生异常语句后的语句不再执行
  2. 若在 finally 块中返回,则 try 或 catch 块中的 return 必定不会执行
public int method() {
   int i = 1;
   try {
	       i++;
	       String[] names = new String[3];
	       if (names[i].equals("lcx")) {
	       }
	       return i;
   } catch (NullPointerException e) {
       return ++i;
   } finally {
       // return 4
       return ++i;
   }
}
public int method1() {
   int i = 1;
   try {
       i++;
       String[] names = new String[3];
       if (names[i].equals("lcx")) {
       }
       return i;
   } catch (NullPointerException e) {
       // return 3。 i = 3 时将结果存入临时空间,finally 执行完后返回临时空间的值 3
       return ++i;
   } finally {
       ++i;
   }
}

4.3 自定义异常

  1. 如果继承 Exception,则属于编译异常,必须显式处理或抛出
  2. 如果继承 RuntimeException,则属于运行时异常,如果处理则默认 throws
  3. 子类重写父类的方法时,子类抛出的异常需小于等于父类定义的异常类型

4.4 throws 和 throw

含义

位置

抛出内容

throws

异常处理的一种方式

方法声明处

异常类型

throw

手动生成异常对象

方法体中

异常对象

4.5 泛型介绍

  1. 泛型的优点:
  • 不使用泛型的弊端:不能对加入到集合中的数据类型进行约束(不安全)、遍历集合时需要进行显式类型转换
  • 使用泛型的好处:编译时检查元素的类型提高了安全性、减少了类型转换的次数提高了运行效率
  1. 泛型定义:泛型又称为 参数化类型,是 jdk5.0 出现的新特性,解决数据类型的安全性问题,只需在类声明或实例化时指定具体需要的类型即可,编译期即可确定类型。Java 泛型保证在编译期没有发出警告的代码,在运行阶段就不会发生类转换异常,使得代码更加简洁与健壮
  2. 泛型的使用规则:可以在类声明时通过一个标识符表示类中某个属性的类型,或是某个方法的返回值类型,或是参数类型。指定泛型类型时只能是 引用类型。在给泛型指定具体类型后,可以传入该类型或其子类类型。省略指定泛型类型时,默认填充 Object

4.6 泛型类型

  1. <?>:支持任意泛型类型
  2. <? extends A>:规定了泛型的上限,支持 A 类以及 A 的子类
  3. <? super A>:规定了泛型的下限,支持 A 类以及 A 的直接或间接父类

4.7 自定义泛型

  1. 自定义泛型的注意事项:
  • 静态方法、静态字段中不能使用类的泛型
  • 使用泛型的数组,不能进行初始化
  • 泛型不具备继承性,即以下代码语法错误 List<Object> lists = new ArrayList<String>();
  1. 泛型方法可以定义在普通类中、也可以定义在泛型类中
/**
 * 泛型类
 *
 * @author Spring-_-Bear
 * @datetime 5/28/2022 8:57 AM
 */
public class Generic<R> {
    private R r;

    public R getR() {
        return r;
    }

    // 泛型方法
    public <T> T getT(T t) {
        return t;
    }
}
  1. 泛型接口的类型,在继承接口或是实现接口时确定
interface IGeneric<T> {
    T getT();
}

// 实现接口时指定接口的泛型
class GenericImpl implements IGeneric<Integer> {
    @Override
    public Integer getT() {
        return 3;
    }
}

// 继承接口时指定接口的泛型
interface IGeneric2 extends IGeneric<String> {
}

五、常用类

5.1 包装类

  1. 八大包装类都是 final 类。除 Boolean 和 Character 外,其余包装类均继承父类 Number
  2. 装箱与拆箱:jdk5 以前是手动装箱和拆箱。自动装箱底层调用的是对应包装类的 valueOf() 方法
// 手动装箱
Integer integer = Integer.valueOf(23);
// 手动拆箱
int a = integer.intValue();
  1. String 与包装类:
  • String 转包装类
String str = "123";
// 方式 1
Integer i = Integer.parseInt(str);
// 方式 2 
Integer i = new Integer(str);
  • 包装类转 String
Integer i = 100;
// 方式 1
String str = i + "";
// 方式 2
String str = i.toString();
// 方式 3
String str = String.valueOf(i);
  1. Integer 的创建机制:自动装箱机制,在 [-128,127] 范围内直接返回,否则 new Integer(i)
/**
 * Integer 包装类装箱源码
 */
public static Integer valueOf(int i) {
   if (i >= IntegerCache.low && i <= IntegerCache.high)
       return IntegerCache.cache[i + (-IntegerCache.low)];
   return new Integer(i);
}
/* 经典例题 */
Integer i = new Integer(1);
Integer ii = new Integer(1);
// false
System.out.println(i == ii);

Integer j = 1;
Integer jj = 1;
// true
System.out.println(j == jj);

Integer k = 128;
Integer kk = 128;
// false
System.out.println(k == kk);

// 只要有基本数据类型参与比较,判断的是值是否相等
Integer j = 128;
int jj = 128;
// true
System.out.println(j == jj);

5.2 String

  1. String 是一个线程不安全的 final 类,其字段 value[] 是 final 类型的
  2. String 的创建机制:
  • String s1 = "lcx"; 先查找常量池是否有 “lcx” 数据空间,如果有则直接将栈变量 s1 指向 “lcx”;没有则新建后指向,s1 最终指向的是常量池的空间地址
  • String s2 = new String("lcx"); 先在堆中创建空间,维护了 String 类字段 value[],然后判断常量池中是否有 “lcx” 的数据空间。如果常量池没有 “lcx”,则新建后使用 value 指向;如果有,则 value 直接指向。s2 最终指向的是堆中 value 的地址

java的stu java的study_java的stu_05

  • String 创建机制经典例题:
String a = "lcx";
String b = new String("lcx");
// true:a 指向常量池中 "lcx" 的地址,b.intern() 返回常量池中 "lcx" 的地址
System.out.println(a == b.intern());
// false:b 返回 String 类的字段 value[] 的地址,b.intern() 返回常量池中 "lcx" 的地址
System.out.println(b == b.intern());

String s1 = new String("abc");
String s2 = new String("abc");
// false:s1,s2 -> 不同的 value[]
System.out.println(s1 == s2);
// true:引用到常量池的同一个地址
System.out.println(s1.intern() == s2.intern());
  1. String 的相加:常量相加看池,变量相加看堆
// 只创建了一个字符串常量 "hello123"
String a = "hello" + "123";
// b -> 常量池的 "hello"
String b = "hello";
// c -> 常量池的 "123"
String c = "123";
// d -> 堆中 value,value 指向常量池中的 "hello123";b + c 的底层实现:调用 StringBuilder 的 append 方法连接两次,然后再 new String() 返回
String d = b + c;

public class Test {
    // str 指向堆中的 value,value 指向常量池中的 "lcx"
    String str = new String("lcx");
    // 字符数组对象存放于堆中,final 表示 ch 的指向不能改变
    final char[] ch = {'j','a','v','a'};
    
    public void change(String str, char[] ch) {
       // 在常量池中新建 "java" 的数据空间,change 方法栈中的 str 指向 "java"
       str = "java";
       // 方法栈中的 ch 指向堆中的 final char[]
       ch[0] = 'h';
    }
    public static void main(String[] args) {
       Test test = new Test();
       test.change(test.str, test.ch);
       // Output: lcx and hava
       System.out.print(test.str + " and " + test.ch);
    }
}

java的stu java的study_JDBC_06

5.3 StringBuffer

  1. StringBuffer 是一个线程安全的 final 类,其字段 value[] 不是 final 类型的
  • String 保存的是字符串常量,value 引用到常量池
  • StringBuffer 保存的是字符串变量,数据存放在堆中
  • StringBuilder 线程不安全,用法与 StringBuffer 基本一致
  • StringBuffer value[] 无参构造器默认初始化容量为 16;若构造器传入的是字符串,则初始化容量为字符串长度加上 16

java的stu java的study_Java_07

  1. StringBuffer 的使用注意事项:
String str = null;

StringBuffer stringBuffer = new StringBuffer();
// append 方法可以追加 null
stringBuffer.append(str);
// Output:null
System.out.println(stringBuffer);

// 构造器不允许传入空,否则抛出 java.lang.NullPointerException
stringBuffer = new StringBuffer(str);
System.out.println(stringBuffer);

5.4 Arrays

  1. 使用 Arrays.binarySearch() 方法对有序数组进行二分查找时,若不存在该元素,则返回该元素应在数组中位置下标的负值
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
                                 long key) {
    int low = fromIndex;
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        long midVal = a[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found.
}
  1. Arrays.asList() 可以将数组转换为 List 类型的集合,运行类型为 Arrays$ArrayList,即 Arrays 类中的静态内部类 ArrayList
List<Integer> list = Arrays.asList(1, 2, 3);
// output: class java.util.Arrays$ArrayList
System.out.println(list.getClass());

Integer[] array = new Integer[]{1, 2, 3};
List<Integer> integerList = Arrays.asList(array);

int[] arr = new int[]{1, 2, 3};
List<int[]> ints = Arrays.asList(arr);
  1. 定制排序:
public class CustomizationSort {
    public static void main(String[] args) {
        Integer[] arrays = new Integer[]{1, 31, 523, 452, 13, 64, 23, 75};
        CustomizationSort sort = new CustomizationSort();
        
        // 面向接口编程 + 动态绑定(多态)
        sort.bubble(arrays, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });
        System.out.println(Arrays.toString(arrays));
    }

    public void bubble(Integer[] arrays, Comparator<Integer> comparator) {
        int len = arrays.length;
        for (int i = 0; i < len - 1; i++) {
            for (int j = 0; j < len - 1 - i; j++) {
                if (comparator.compare(arrays[j], arrays[j + 1]) > 0) {
                    int temp = arrays[j];
                    arrays[j] = arrays[j + 1];
                    arrays[j + 1] = temp;
                }
            }
        }
    }
}

5.5 BigDecimal

BigDecimal bigDecimal = new BigDecimal("242131.24321335243234123");
// 为避免结果为无限循环小数,可对结果指定精度以解决 ArithmeticException
BigDecimal res = bigDecimal.divide(new BigDecimal("1.1"), BigDecimal.ROUND_CEILING);
System.out.println(res);

5.6 日期类

  1. JDK1.0 中出现的 java.util.Date 类,大多数方法在 jdk1.1 引入的 Calendar 类中已被弃用,第二代日期类 Calendar 也存在着以下一些问题:
  • 可变性:像日期和时间这样的类应该是不可变的
  • 偏移性:年份从 1900 开始,月份从 0 开始
  • 格式化:不能对 Calendar 进行格式化
  • 线程不安全,不能处理闰秒(每隔两天,多出 1s)
  1. JDK8 引入了第三代日期类:LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间)
LocalDate localDate = LocalDate.now();
// output: 2022-09-22
System.out.println(localDate);
LocalTime localTime = LocalTime.now();
// output: 11:31:36.591
System.out.println(localTime);
LocalDateTime localDateTime = LocalDateTime.now();
// output: 2022-09-22T11:32:13.159
System.out.println(localDateTime);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// output: 2022-09-22 11:33:05
System.out.println(formatter.format(localDateTime));

Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// output: 2022-09-22 11:34:14
System.out.println(simpleDateFormat.format(date));

六、集合类

6.1 Collection

java的stu java的study_IO_08

java的stu java的study_Socket_09

6.2 Iterator

  1. Iterator 称为迭代器,主要用于遍历 Collection 集合中的元素。所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用来返回一个迭代器
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

Iterator<Integer> iterator = list.iterator();
// 判断是否还有下一个元素
while (iterator.hasNext()) {
    // 取出当前元素
    System.out.println(iterator.next());
    // 从集合中移除当前元素
    iterator.remove();
}
// output: 0
System.out.println(list.size());
  1. Iterator 对象仅用于遍历集合,本身并不存放数据对象。使用增强 for 循环遍历集合时,底层仍然使用的 Iterator 进行迭代遍历

6.3 List

  1. 实现 List 接口的集合类元素添加与取出顺序一致,允许元素重复,支持使用对应的索引值直接获取元素的值,可动态扩充容量
  2. ArrayList:
  • 底层机制:transient Object[] elementData;
  • 扩容机制:当超过 elementData 容量时,扩容为原有大小的 1.5
  • 无参构造器:elementData 初始容量为 0,第一次添加元素后容量为 10
  • 有参构造器:elementData 的初始容量为指定大小
// overflow-conscious code
int oldCapacity = elementData.length;
// 在原有大小基础上加上原有大小的一半为新容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • ArrayList 线程不安全,可以添加多个 null 元素
  1. Vector:
  • 底层机制: protected Object[] elementData;
  • 扩容机制:当超过 elementData 容量时,扩容为原有大小的 2
  • 无参构造器:elementData 初始容量为 0,第一次添加元素后容量为 10
  • 有参构造器:elementData 的初始容量为指定大小
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) capacityIncrement : oldCapacity);
  • Vector 线程安全,可以添加多个 null 元素
  1. LinkedList:
  • 底层机制:双向链表和双端队列(Queue)。类中有两个属性 transient Node<E> firsttransient Node<E> last 分别指向头节点和尾节点,每个节点中又维护了 prev、next、item 三个属性
  • 特点:LinkedList 线程不安全,可以添加多个 null 元素;具有双端队列的特点可头插、头删、尾插、尾删等

6.4 Map

  1. Map 与 Collection 接口并列存在,用于保存具有映射关系的键值对 key - value,Map 中的 key 和 value 可以是任何引用类型。Map 具有以下特点:
  • key 不允许重复,value 可以重复,key 相同时用新的 value 替换旧的 value
  • key 最多有一个 null,而 value 可以有多个 null

java的stu java的study_Java_10

  1. HashMap:
  • 底层机制:JDK1.8 版本的 HashMap 底层是【数组 + 链表 + 红黑树】,而 JDK1.7 版本的 HashMap 底层是【数组 + 链表】
  • 扩容机制:默认初始化容量为 16,加载因子 loadFactor = 0.75,临界值 threshold = 12
  • 树化条件:哈希表长度 不小于 64 且某颗链表的长度 不小于 8 就将该颗链表树化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
static final int TREEIFY_THRESHOLD = 8;
  • 剪枝:若某颗红黑树元素个数较少,则会触发剪枝行为即将红黑树重新转换为链表
  • HashMap 线程不安全,key-val 封装在 HashMap$Node 中;key 最多有一个 null,而 value 可以有多个 null
  1. HashMap 元素的存放过程:
  • 先通过 HashMap 类的静态方法 hash(Object) 获得本次元素的 hash 值
// 确定元素 hash 值的算法
static final int hash(Object key) {
   int h;
   // key 的 hashCode 与 key 的 hashCode 无符号右移 16 位的值做按位异或运算得到元素的 hash 值
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 将 hash 值与本次哈希表大小 -1 的值进行按位与运算获得本次添加的元素在哈希表中的位置号,若该位置上没有其它元素则直接存放
// 用本次哈希表长度减 1 与本次元素的 hash 值进行按位与运算获得元素在哈希表中的位置号
if ((p = tab[i = (n - 1) & hash]) == null)
 // 位置号上未存储元素则直接存放
 tab[i] = newNode(hash, key, value, null);
  • 若位置上已经存在元素,则遍历该条链表判断是否已经存在相同 key,若已存在则返回,否则创建新的节点连接到链表尾(树化判断)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
   Node<K,V>[] tab; Node<K,V> p; int n, i;
   // 第一次扩容:table 为 HashMap 的静态内部类 Node 类型的数组 transient Node<K,V>[] table;
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length; 

   // 用本次哈希表长度减 1 与本次元素的 hash 值进行按位与运算获得元素在哈希表中的位置号
   if ((p = tab[i = (n - 1) & hash]) == null)
       // 位置号上未存储元素则直接存放
       tab[i] = newNode(hash, key, value, null);
   else {
       // 位置号上已经存放元素,判断当前要添加的元素是否存在相同元素
       Node<K,V> e; K k;
       // 如果当前元素的 hash 值与哈希表位置号上元素的 hash 值相同
       // 且引用相同或者要添加的元素不为空且 equals 比较相同,则要添加的元素与当前位置号上的元素相同
       if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
           e = p;
       // 判断当前位置号是否是红黑树数据结构,是则按照红黑树方式添加
       else if (p instanceof TreeNode)
           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       else {
           // 遍历此位置号上的链表元素,判断是否与当前需要加入的元素相同
           for (int binCount = 0; ; ++binCount) {
               // 不相同,添加到链表尾
               if ((e = p.next) == null) {
                   p.next = newNode(hash, key, value, null);
                   // 判断该条链表上的元素个数是否 >= 8个,是则进行树化条件判断
                   if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                       treeifyBin(tab, hash);
                   break;
               }
               // 如果存在相同的元素则不添加
               if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                   break;
               p = e;
           }
       }
       // 当前要添加的元素已存在,则不添加,返回旧值(新旧交替)
       if (e != null) { // existing mapping for key
           V oldValue = e.value;
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
           afterNodeAccess(e);
           return oldValue;
       }
   }
   ++modCount;
   // size 为哈希表中的元素个数,判断加入的元素个数是否不小于临界值,是则对哈希表进行扩容
   if (++size > threshold)
       resize();
   // HashMap 的空方法,留给子类实现以扩展功能	
   afterNodeInsertion(evict);
   // 返回 null 代表元素添加成功
   return null;
}
  1. Hashtable:
  • 底层机制:使用方法与 HashMap 基本一致,散列表类型为 Hashtable$Entry
  • 扩容机制:默认初始化容量为 11,,加载因子 0.75,按原有容量的 2 倍加 1 的机制进行扩容
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
  • Hashtable 线程安全,键和值都不能为 null,否则抛出 NullPointerException
Hashtable<String, Integer> hashtable = new Hashtable<>();
// java.lang.NullPointerException
hashtable.put(null, 2);
// java.lang.NullPointerException
hashtable.put("Spring-_-Bear", null);
  1. LinkedHashMap:
  • 底层机制:继承自 HashMap。数组的类型是 HashMap$Node[],其中存放的元素是 LinkedHashMap$Entry 类型。在 HashMap 的基础之上,增加了一个 双向链表 用来记录元素的添加顺序,使得元素看起来是以插入顺序保存的
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
  • 特点:LinkedHashMap 线程不安全,key 最多有一个 null,而 value 可以有多个 null;遍历 LinkedHashMap 时可以使元素的输出顺序与插入顺序一致
  1. TreeMap:
  • 底层机制:TreeMap 实现了 SortedMap 接口,即 TreeMap 中插入的元素是有顺序的
  • 使用 TreeMap 时,放入的 key 必须实现Comparable接口以自定义元素比较规则从而实现元素排序存放
  • 去重机制:
  • 如果传入了 Comparator 匿名对象,则使用实现的 compare 方法中的比较方式去重,compare 方法返回 0 则认为是相同的元素
  • 如果没有传入匿名比较器对象,则以所要添加的对象实现的 Comparaeable 接口的 compareTo 方法比较机制去重

6.5 Set

  1. Set 中的元素无序(hash 后确定存储位置),不存在索引。Set 不允许重复元素,故最多只包含一个 null
// 1. 增强 for 遍历 Set
for (Integer integer : set) {
    System.out.println(integer);
}

// 2. Iterator 遍历 Set
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}
  1. HashSet:底层采用 HashMap,value 为系统给定的 PRESENT Object 对象
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
  1. LinkedHashSet:底层使用 LinkedHashMap,底层维护双向链表使得元素取出顺序与添加顺序一致
  2. TreeSet:底层使用 TreeMap,实现元素添加后有序存放

6.6 集合遍历

  1. List 遍历
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

// 方式一:普通 for
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
// 方式二:增强 for
for (Integer integer : list) {
    System.out.println(integer);
}
// 方式三:iterator
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
    iterator.remove();
}
  1. Map 遍历
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);

// 方式一:entrySet + 增强 for:key-value 的值实际存放到 HashMap$Node 中,而 HashMap$Node 实现了 Map.Entry<K,V> 接口,所以可通过 EntrySet 拿到每一个 Entry,再从 Entry 中依次取出 key 和 value
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
for (Map.Entry<Integer, Integer> entry : entries) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 方式二:entrySet + iterator
Iterator<Map.Entry<Integer, Integer>> iterator = entries.iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, Integer> entry = iterator.next();
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 方式三:Lambda
map.forEach((k, v) -> System.out.println(k + " -> " + v));

// 单独遍历 key 和 value:key-value 的值实际存放到 HashMap$Node 中,为方便单独遍历 key 或 value,使用 KeySet(Set 类型) 引用到 Node 中所有的 key,使用 Values (Collection 类型)引用到所有的 value
Set<Integer> keySet = map.keySet();
for (Integer integer : keySet) {
    System.out.println(integer);
}
Collection<Integer> values = map.values();
for (Integer value : values) {
    System.out.println(value);
}
  1. Set 遍历
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);

// 方式一:增强 for
for (Integer integer : set) {
    System.out.println(integer);
}
// 方式二:iterator
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

6.7 集合选择

  1. 存储单个对象且允许重复选择 List

应用场景

集合选择

底层结构

备注

改查多

ArrayList

Object[]

默认 size = 0,第一次添加 size = 10;按 1.5 倍扩容

增删多

LinkedList

双端队列

多线程

Vector

Object[]

默认 size = 0,第一次添加 size = 10;按 2 倍扩容

  1. 存储单个对象且不允许重复选择 Set

应用场景

集合选择

底层使用

无顺序要求

HashSet

HashMap

添加、取出顺序一致

LinkedHashSet

LinkedHashMap

元素有序排列存放

TreeSet

TreeMap

  1. 键值对 key-value 选择 Map

应用场景

集合选择

底层结构

备注

键顺序无要求

HashMap

数组+链表+红黑树

size = 16,loadFactor = 0.75,按 2 倍扩容

插入、取出顺序一致

LinkedHashMap

数组 + 双向链表

继承自 HashMap

多线程

Hashtable

数组+链表+红黑树

size = 11,loadFactor = 0.75,按 2 倍 + 1 扩容,k-v 不允许 null

配置文件

Properties

数组+链表+红黑树

继承自 Hashtable

键有序存放

TreeMap

红黑树

6.8 Collections

Collections 是一个操作 Set、List、Map 等集合的工具类,提供了一系列的静态方法对集合进行操作

方法

功能

reverse(List)

反转 List 中的元素顺序

shuffle(List)

对 List 中的元素进行随机排序

sort(List)

按元素自然顺序方式对 List 中的元素排序

sort(List,Comparator)

根据 Comparator 的顺序对 List 中的元素排序

swap(List,int,int)

对 List 中的两个元素交换顺序

max(Collection)

自然顺序找出集合中的最大元素

max(Collection,Comparator)

指定排序找出集合中的最大元素

frequency(Collection, Object)

某个元素在集合中的出现次数

copy(List,List)

List 拷贝

replaceAll(List,Object,Object)

用新的 Object 替换指定的 Object

七、多线程

7.1 线程概述

  1. 进程:进程是程序的一次执行过程,是一个动态过程,有其自身的产生、存在和消亡历程
  2. 线程:线程是由进程创建的,是进程的一个实体,一个进程可以拥有多个线程
  3. 并发:同一时刻多个任务交替执行,造成一种 “貌似同时” 的错觉。简单地说,单核 CPU 实现的多任务就是并发
  4. 并行:同一时刻多个任务同时执行,多核 CPU 实现的多任务就是并行
  5. Java 获取 JVM 可用处理器个数:
// 获取 JVM 可用处理器个数
Runtime runTime = Runtime.getRuntime();
System.out.println(runTime.availableProcessors());

7.2 线程创建

  1. 继承 Thread 类创建线程:
/**
 * @author Spring-_-Bear
 * @version 2021-11-21 19:53
 */
public class ThreadEx extends Thread {
    // 当启动程序时理解为开启了一个进程,进程开启了 main 线程
    public static void main(String[] args) {
    // 在 main 线程中可以开启其它线程,只有当所有线程都消亡时,进程才结束
        new Dog().start();
        for (int i = 1; i <= 10; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + i);
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Dog extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println(Thread.currentThread().getName() + ":汪汪汪~~~");
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 实现 Runnable 接口创建线程:
  • Java 是单继承机制,若某个类已经存在父类,这时可以通过实现 Runnable 接口实现线程
  • 实现 Runnable 接口的类不能直接调用 start() 方法,可以将对象作为 new Thread(Runnable) 的参数,从而调用 start() 方法(静态代理设计模式)
  • 实现 Runnable 接口方式更加适合 多个线程共享某个资源 的情况,并且避免了单继承的局限
/**
 * @author Spring-_-Bear
 * @version 2021-11-21 20:50
 */
public class Thread02 {
    public static void main(String[] args) {
    // 静态代理设计模式
        new Thread(new Cat()).start();
    }
}

class Cat implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("喵喵喵~~~");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.3 start 与 run

new Dog().run()new Dog().start() 的区别:

  • 前者只是单纯地调用 run() 方法
  • 后者启动了线程,在 start() 方法中实际调用了 start0() 方法,这是个 native 修饰的方法,真正实现了线程启动。start() 方法调用了 start0() 方法后,该线程并不一定会马上执行,只是将线程变成了就绪状态,具体什么时候执行,取决于操作系统处理机调度

7.4 Thread

方法

功能

setName(String)

设置线程名

getName()

获取线程名

start()

启动线程

run()

调用线程对象的 run() 方法

setPriority(int)

设置线程优先级(1->5->10)

getPriority()

获得线程优先级

sleep(long)

休眠线程

interrupt()

中断线程,线程并未消亡

7.5 线程插队与礼让

方法名

功能

yield()

线程礼让,让出 CPU 资源,礼让时间不确定,是否礼让成功不确定,取决于 OS

join()

线程插队,一旦插队成功,则必须执行完插队线程的所有任务

/**
 * @author Spring-_-Bear
 * @version 2021-11-21 22:49
 */
public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread task = new Thread(new Task());
        task.start();

        for (int i = 1; i <= 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            Thread.sleep(1000);
            if (i == 3) {
                // task 线程插队,一旦插队成功则需先执行完所有 task 任务
                // task.join();
                // main 线程礼让,不一定礼让成功
                Thread.yield();
            }
        }
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.6 守护线程

  1. 用户线程:也称工作线程,当线程的任务执行完成后结束或以通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束(经典守护线程:垃圾回收机制)
/**
 * @author Spring-_-Bear
 * @version 2021-11-22 19:10
 */
public class ThreadDaemon {
    public static void main(String[] args) throws InterruptedException {
        Thread task = new Thread(new Task());
        // 设置为 main 线程的守护线程
        task.setDaemon(true);
        task.start();
        Thread.sleep(5000);
        // 工作线程结束,守护线程自动终止
        System.out.println("妈妈回家了,小明结束写作业!");
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("小明写作业中···");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.7 线程的七大状态

java的stu java的study_Socket_11

状态

说明

NEW

尚未启动的线程处于此状态。 该状态线程对象被创建,但还未调用 start 方法

READY

就绪状态,等待 JVM 的调度

RUNNABLE

可运行状态,细分为 RunningReady 两个状态。处于可运行状态的线程可能正在 Java 虚拟机中执行,也可能正在等待来自操作系统的某些资源

BLOCKED

被阻塞等待监视器锁的线程处于此状态。处于该状态的线程正在等待获取一个监视器锁进入同步代码或方法;也可能是在调用了 Object.wait() 后等待一个监视器锁重新进入同步代码或方法

WAITING

线程处于等待状态;处于该状态的线程可能是因为调用了 Object.wait()、Thread.join()、LockSupport.park() 中的某一个方法;处于该状态的线程正在等待其他线程完成一些特定的操作

TIMED_WAITING

正在等待另一个线程执行动作达到指定等待时间的线程处于此状态;处于这个状态的线程可能是调用了具有指定时间的 Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()

TERMINATED

已退出的线程处于此状态

7.8 线程同步机制

  1. 线程互斥:当有一个线程正在对内存进行操作时,其它线程都不可以对这个内存进行操作,直到该线程完成操作其它线程才能对该内存进行操作,也即同一时刻只允许一个线程操作内存
  2. 对象互斥锁:Java 语言中引入了对象互斥锁来保证共享数据操作的完整性。每个对象都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻有且仅有一个线程可以访问该对象,用关键字 synchronized 来实现对象的互斥锁
  3. 同步代码块与同步方法:
  • 同步代码块:将 synchronized 加在某段代码上,表明此段代码是同步代码
  • 同步方法:得到对象的锁才可以操作对象的代码。可以将 synchronized 加在方法声明中,表示整个方法为同步方法
  1. 静态同步方法与非静态同步方法:
  • 静态同步方法的锁是当前类本身(ClassName.class
/**
 * 静态方法中的互斥锁加在类上
 */
class SellTicket implements Runnable {
   public static void getTicket() {
       synchronized (SellTicket.class) {
           System.out.println("售出一张票");
       }
   }
}
  • 非静态同步方法的锁可以是它本身 this,也可以是其它对象(要求是同一个对象)
class SellTicket implements Runnable {
    private int ticketNum = 100;
    private boolean hasTicket = true;
    private final Object object = new Object();

    public void sell() {
        // 锁 this 与锁 object 等价
        synchronized (object) {
            if (ticketNum <= 0) {
                System.out.println(Thread.currentThread().getName() + " 售票结束...");
                hasTicket = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
        }
        // 释放锁后再睡觉
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (hasTicket) {
            sell();
        }
    }
}

7.9 线程死锁

  1. 线程死锁:两个或两个以上的线程在执行过程中同时被阻塞,它们中的某个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程被称为线程死锁
  2. 产生死锁必须满足的四个条件:
  • 互斥条件:只有对需要互斥使用的资源的争夺才会发生死锁
  • 不剥夺条件:进程所获得的资源在未使用完之前,不能由其它进程强行夺走,只能主动释放
  • 请求和保持条件:进程已经保持了至少一个资源并持有不放,但又提出了新的资源请求,而该资源被其它进程占有,此时请求进程将被阻塞
  • 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求(循环等待未必死锁,死锁一定有循环等待)
  1. 线程死锁 Java 代码示例:
/**
 * @author Spring-_-Bear
 * @version 2021-11-22 21:02
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        new DeadLock(true).start();
        new DeadLock(false).start();
    }
}

class DeadLock extends Thread {
    static final Object object1 = new Object();
    static final Object object2 = new Object();
    boolean flag;

    public DeadLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (object1) {
                System.out.println(Thread.currentThread().getName() + " 获得 object1 的锁,尝试获取 object2 的锁···");
                synchronized (object2) {
                    System.out.println(Thread.currentThread().getName() + " 成功获得 object2 的锁!");
                }
            }
        } else {
            synchronized (object2) {
                System.out.println(Thread.currentThread().getName() + " 获得 object2 的锁,尝试获取 object1 的锁···");
                synchronized (object1) {
                    System.out.println(Thread.currentThread().getName() + " 成功获得 object1 的锁!");
                }
            }
        }
    }
}
  1. 线程释放锁的情况:
  • 当前线程的同步方法、同步代码块中 执行结束 会释放锁
  • 当前线程在同步方法、同步代码块中遇到 break、return 会释放锁
  • 当前线程在同步方法、同步代码块中出现了 未处理的 Error 或 Exception 导致结束 会释放锁
  • 当前线程在同步方法、同步代码块中执行了线程 对象的 wait() 方法,当前线程暂停并释放锁
  1. 不释放锁的情况:
  • 当前线程在同步方法、同步代码块中调用了 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行不会释放锁
  • 线程执行同步代码块时,其它线程调用了该线程的 suspend() 方法将该方法挂起,该线程不会释放锁(suspend()、resume() 方法均已过时,不再推荐使用)

7.10 多线程卖票

  1. 未使用同步锁出现超卖现象
/**
 * @author Spring-_-Bear
 * @version 2021-11-21 21:48
 */
public class Ticket {
    public static void main(String[] args) {
        // 共享总票数这一资源
        SellTicket sellTicket = new SellTicket();
        Thread thread = new Thread(sellTicket);
        Thread thread1 = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        thread.start();
        thread1.start();
        thread2.start();
    }
}

class SellTicket implements Runnable {
    private int ticketNum = 10;

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票结束···");
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + "售出一张票,余票:" + (--ticketNum));
        }
    }
}
  1. 锁方法,抱着锁睡觉,未均匀卖票,Why?执行任意一行输出即可均匀卖票?
/**
 * 锁方法,抱着锁睡觉,未均匀卖票,Why?执行任意一行输出即可均匀卖票?
 *
 * @author Spring-_-Bear
 * @datetime 2022/4/10 15:43
 */
public class Ticket {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
    }
}

class SellTicket implements Runnable {
    private int ticketNum = 100;
    private boolean hasTicket = true;

    public synchronized void sell() {
        if (ticketNum <= 0) {
            System.out.println(Thread.currentThread().getName() + " 售票结束...");
            hasTicket = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
        try {
            // 抱着锁睡觉,睡醒后释放锁不公平竞争?
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (hasTicket) {
            // System.out.println(Thread.currentThread().getName());
            sell();
        }
    }
}
  1. 锁代码块,线程不抱着锁睡觉,10 个线程均匀售票
/**
 * 锁代码块,未抱着锁睡觉,10 个线程均匀卖票
 *
 * @author Spring-_-Bear
 * @datetime 2022/4/10 15:43
 */
public class Ticket {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
    }
}

class SellTicket implements Runnable {
    private int ticketNum = 100;
    private boolean hasTicket = true;

    public void sell() {
        synchronized (this) {
            if (ticketNum <= 0) {
                System.out.println(Thread.currentThread().getName() + " 售票结束...");
                hasTicket = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
        }
        // 释放锁后再睡觉
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (hasTicket) {
            sell();
        }
    }
}

八、IO

8.1 File

方法名

功能

new File(String)

根据路径构建一个 File 对象

new File(File,String)

根据父目录文件 + 子路径构建

new File(String,String)

根据父路径 + 子路径构建

getName()

获取文件名,包含文件扩展名

getAbsolutePath()

获取文件绝对路径

getParent()

获取文件父级目录

length()

获取文件大小,以字节为单位

exists()

判断是否存在

isFile()

判断是否是文件

isDirectory()

判断是否是目录

mkdir()

创建一级目录

mkdirs()

创建多级目录

delete()

删除空目录或文件

8.2 IO 流的分类

  1. 流的定义:数据在数据源(文件)和程序(内存)之间经历的路径。以程序(内存)为参照,流入内存为输入流,流出内存为输出流
  2. IO 流分类:
  3. IO 流的四大抽象基类:
  • InputStream(字节输入流)
  • OutputStream(字节输出流)
  • Reader(字符输入流)
  • Writer(字符输出流)

java的stu java的study_Java_12

8.3 字节流与字符流

  1. 字节流文件拷贝:
String src = "C:\\Users\\Admin\\Desktop\\BeFree.jpg";
String dst = "C:\\Users\\Admin\\Desktop\\BeFree.png";
FileInputStream fileInputStream = new FileInputStream(src);
// 无需追加写入,因为并未再次打开文件
FileOutputStream fileOutputStream = new FileOutputStream(dst);
int readLen;
byte[] buf = new byte[1024];
while ((readLen = fileInputStream.read(buf)) != -1) {
    fileOutputStream.write(buf, 0, readLen);
}
fileOutputStream.close();
fileInputStream.close();
  1. 字符流使用:字符流使用完毕后必须 close() 或手动 flush(),否则数据只是暂存在内存缓冲区中,并未写入文件。字符流底层调用字节流实现具体功能
// 字符流追加写入文件
FileWriter fileWriter = new FileWriter("c:/users/admin/desktop/test.txt", true);
fileWriter.write("Hello World");
fileWriter.write(" World");
fileWriter.close();

8.4 节点流与处理流

  1. 节点流:节点流是底层流,直接对数据源进行操作,可以从一个特定的数据源读写数据。如 FileReader、FileWriter
  2. 处理流:也称包装流,是 “连接” 已存在的节点流,为程序提供更为强大的读写功能。如 BufferedReader、BufferedWriter
  • 使用处理流时,只需关闭外层流即可,对应的节点流会自动关闭
  • 处理流设计理念体现了修饰器模式

java的stu java的study_Java_13

  1. 使用处理流的好处:使用处理流包装节点流,既可以消除不同节点流的实现差异,也可以提供更加方便的方法来完成文件操作
  • 性能提高:主要以增加缓冲的方式来提高输入、输出的效率
  • 操作便捷:提供了一系列便捷的方法来一次输入、输出大批量的数据
  1. 节点流和处理流:

分类

字节输入流

字节输出流

字符输入流

字符输出流

类型

抽象基类

InputStream

OutputStream

Reader

Writer

节点流

访问文件

FileInputStream

FileOutputStream

FileReader

FileWriter

节点流

访问数组

ByteArrayInputSteam

ByteArrayOutputStram

CharArrayReader

CharArrayWriter

节点流

访问管道

PipedInputStream

PipedOutputStream

PipedReader

PipedWriter

节点流

访问字符串

-

-

StringReader

StringWriter

节点流

缓冲流

BufferedInputStream

BufferedOutputStream

BufferedReader

BufferedWriter

处理流

转换流

-

-

InputStreamReader

OutputStreamWriter

处理流

对象流

ObjectInputStream

ObjectOutputStream

-

-

处理流

抽象基类

FilterInputStream

FilterOutputStream

FilterReader

FilterWriter

处理流

打印流

-

PirntStream

-

PrintWriter

处理流

推回输入流

PushbackInputStream

-

PushbackReader

-

处理流

特殊流

DataInputStream

DataOutputStream

-

-

处理流

8.5 对象处理流

  1. 序列化与反序列化:
  • Java 序列化是指把 Java 对象转换为字节序列的过程
  • Java 反序列化是指把字节序列恢复为 Java 对象的过程
  1. 序列化接口:若某个对象需支持序列化机制,则必须让其类是可序列化的,也即必须实现 SerializableExternalizable 接口之一
  • Serializable 是标记接口,无需实现任何方法
  • Externalizable 继承自 Serializable,需要实现方法
  1. 序列化机制:
  • 序列化的类建议添加 SerialVersionUID 序列化版本 ID 以提高版本的兼容性
  • 序列化对象时,默认将除了 static、transient 修饰的成员以外的所有成员进行序列化保存
  • 序列化要求类的所有字段类型也需要实现序列化接口
  • 序列化具备可继承性,即子类继承已序列化的父类,则子类也可以序列化
File file = new File("c:/users/admin/desktop/object.dat");
if (!file.exists()) {
    file.createNewFile();
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(new User(1, "spring", "spring"));
objectOutputStream.writeObject(new User(2, "bear", "bear"));
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Object o = objectInputStream.readObject();
System.out.println(o);
o = objectInputStream.readObject();
System.out.println(o);
objectOutputStream.close();

class User implements Serializable {
    private static final long serialVersionUID = 362498820763181265L;

    private Integer id;
    private String username;
    private transient String password;

    public User(Integer id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
  1. 对象序列化追加保存需要注意的问题:java.io.StreamCorruptedException: invalid type code: AC

8.6 标准输入输出流

  1. 标准输入流:System.in 编译类型是 InputStream,运行类型是 BufferedInputStream,标准输入设备默认为键盘
public class System {
    /**
     * The "standard" input stream. This stream is already
     * open and ready to supply input data. Typically this stream
     * corresponds to keyboard input or another input source specified by
     * the host environment or user.
     */
    public final static InputStream in = null;
}
  1. 标准输出流:System.out 编译类型是 PrintStream,运行类型也是 PrintStream,标准输出设备默认为屏幕
public class System {
    /**
     * The "standard" output stream. This stream is already
     * open and ready to accept output data. Typically this stream
     * corresponds to display output or another output destination
     * specified by the host environment or user.
     * <p>
     * For simple stand-alone Java applications, a typical way to write
     * a line of output data is:
     * <blockquote><pre>
     *     System.out.println(data)
     * </pre></blockquote>
     * <p>
     */
    public final static PrintStream out = null;
}
  1. 打印流 PrintStream:涉及字符输出到文件时需要手动 close()flush(),否则内容不会写入到文件
// 设置打印流为标准输出
PrintStream printStream = System.out;
// print 方法底层调用了 write 方法,因此以下两种方式等价
printStream.print("Hello World!");
printStream.write("Hello Java!".getBytes(StandardCharsets.UTF_8));
printStream.close();

// 重定向输出设备为文件
System.setOut(new PrintStream("C:\\Users\\Admin\\Desktop\\printStream.txt"));
System.out.println("Spring-_-Bear");

8.7 转换流

  1. 输入转换流:InputStreamReader -> Reader:可以将 InputStream(字节流输入流)包装成 Reader(字符输入流),同时指定编码
// 将 InputStream 转换为 BufferedReader
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c:\\users\\admin\\desktop\\temp.txt"), StandardCharsets.UTF_8));
String str;
while ((str = bufferedReader.readLine()) != null) {
    System.out.println(str);
}
bufferedReader.close();
  1. 输出转换流:OutputStreamWriter -> Writer:可以将 OutputStream(字节输出流)包装成 Writer (字符输出流),同时指定编码
// 将 OutputStream 转换为 OutputWriter
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("c:/users/admin/desktop/out.txt")));
bufferedWriter.write("Hello World!");
bufferedWriter.close();

8.8 Properties

方法名

功能

load(InputStream | Reader)

加载配置文件

list(PrintStream | PrintWriter)

将数据显示到指定设备

getProperty(String)

根据键获取值

setProperty(String,String)

设置键值对到 Properties 对象

store(OutputStream | Writer,String)

保存到配置文件,含有中文默认保存 Unicode 码值

Properties properties = new Properties();
// 加载配置文件
properties.load(new FileInputStream("c:/users/admin/desktop/jdbc.properties"));
// 打印所有 key-val 到控制台
properties.list(System.out);
// 根据 key 获取 val
System.out.println(properties.getProperty("url"));
// 根据 key 替换 val
properties.setProperty("username", "bear");
// 存储 properties 到新文件
properties.store(new OutputStreamWriter(new FileOutputStream("c:/users/admin/desktop/tmp.properties")), "Copy from jdbc.properties");

九、网络编程

9.1 IP 与域名

  1. IP 地址:用于唯一标识网络中的每台计算机(主机)或路由器的各接口
  • Windows 查看 IP 地址:ipconfig
  • Linux 查看 IP 地址:ifconfig
  • IPV4 用 4 字节共 32 位标识一个 IP 地址(点分十进制)
  • IPV6 用 8 字节共 128 位标识一个 IP 地址
  1. IP 组成及分类:网络地址 + 主机地址

分类

表示方法

范围

A

0 + 7 位网络号 + 24 位主机号

0.0.0.0 ~ 127.255.255.255

B

10 + 14 位网络号 + 16 位主机号

128.0.0.0 ~ 191.255.255.255

C

110 + 21 位网络号 + 8 位主机号

192.0.0.0 ~ 223.255.255.255

D

1110 + 28 位多播组号

224.0.0.0 ~ 239.255.255.255

E

11110 + 27 位留待后用

240.0.0.0 ~ 247.255.255.255

  1. 域名:将域名映射到 IP 地址,一个域名与一个 IP 地址唯一对应

9.2 端口与协议

  1. 端口:用于标识主机特定的网络程序,以整数形式表示,范围是 0 ~ 65535,其中 0 ~ 1024 已被一些知名程序占用
  2. TCP/IP 四层网络体系结构:网络接口层、网际层、运输层、应用层

TCP/IP模型

对应协议

应用层

HTTP、FTP、Telnet、DNS···

传输层

TCP、UDP···

网络层

IP、ICMP、ARP···

物理 + 数据链路层

Link···

java的stu java的study_Socket_14

9.3 InetAddres

方法名

功能

getLocalHost

获取本机 InetAddress 对象

getByName

根据指定主机名 / 域名获取 InetAddress 对象

getHostName

获取 InetAddress 对象的主机名

getHostAddress

获取 InetAddress 对象的 ip 地址

// 获取本机的 InetAddress 对象
InetAddress localHost = InetAddress.getLocalHost();
// Output: DESKTOP-BEAR/10.134.220.95
System.out.println(localHost);
// 通过计算机名获取本机的 InetAddress 对象
InetAddress desktop = InetAddress.getByName("DESKTOP-BEAR");
// Output: DESKTOP-BEAR/10.134.220.95
System.out.println(desktop);
// 获取 InetAddress 对象的主机名
System.out.println(localHost.getHostName());
// 获取 InetAddress 对象的 ip 地址
System.out.println(desktop.getHostAddress());

// 根据指定主机名/域名获取 InetAddress 对象
InetAddress csdn = InetAddress.getByName("www.csdn.net");
// Output: www.csdn.net/39.106.226.142
System.out.println(csdn);
// Output: www.csdn.net
System.out.println(csdn.getHostName());
// Output: 39.106.226.142
System.out.println(csdn.getHostAddress());

9.4 TCP、UDP 与 Socket

  1. TCP(Transmission Control Protocol):传输控制协议。在使用 TCP 协议前,通信双方须建立 TCP 连接(三报文握手)形成传输数据通道,建立成功后可进行大量数据的传输,数据传输完毕需释放已建立的连接(四报文挥手)
  2. UDP(User Datagram Protocol):用户数据报协议。将数据、源地址、目的地封装成数据报,数据传输前不需要建立连接,数据传输完成无需释放资源
  • 每个数据报的大小限制在 64K 以内
  • 不可靠传输服务,数据传输速度较 TCP 快、效率高
  1. Socket(套接字):开发网络应用程序时被广泛使用,以至于成为事实上的标准。通信的两端都要有 Socket,实际上就是实现两台机器间通信的端口。Socket 允许程序把网络连接当作是一个流,数据在两个 Socket 间通过 IO 传输。一般发起通信的应用程序为客户端,等待通信请求的为服务器
  2. 当客户端与服务端建立连接后,实际上客户端也是通过一个临时端口与服务端进行通信的,这个端口号是根据 TCP / IP 协议由系统随机分配的

9.5 TCP 编程

  1. TCP 文件上传:
  • Server.java
/**
 * @author Spring-_-Bear
 * @version 2021-11-16 09:40
 */
public class Server {
    public static void main(String[] args) throws IOException {
        // 监听端口,等待连接
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器已启动,等待连接···");
        Socket socket = serverSocket.accept();

        // 文件大小,单位为字节
        long fileSize = 0;
        // 文件保存路径
        File fileSavePath = new File("C:\\Users\\Admin\\Desktop\\temp.jpg");

        // 从数组通道读取文件字节数组
        int readLen;
        byte[] buffer = new byte[1024];
        InputStream inputStream = socket.getInputStream();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        System.out.println("文件读取中···");
        while ((readLen = inputStream.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, readLen);
            fileSize += readLen;
        }
        System.out.println("文件读取成功,总大小为 " + fileSize + " bytes!");
        // 将 byteArrayOutputStream 中的数据转换为字节数组(文件二进制数据)
        byte[] fileByteArray = byteArrayOutputStream.toByteArray();

        // 将文件字节数组保存到磁盘
        System.out.println("文件保存到本地中···");
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(fileSavePath));
        bufferedOutputStream.write(fileByteArray);
        System.out.println("文件保存到本地成功!保存路径为:" + fileSavePath);

        // 向客户端反馈文件下载保存成功信息
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        System.out.print("发送反馈信息到客户端··· ");
        bufferedWriter.write("文件接收成功,下载保存完成!");
        bufferedWriter.newLine();
        bufferedWriter.flush();
        System.out.println("消息发送成功!");

        bufferedWriter.close();
        bufferedOutputStream.close();
        byteArrayOutputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
    }
}
  • Client.java
/**
 * @author Spring-_-Bear
 * @version 2021-11-16 09:40
 */
public class Client {
    public static void main(String[] args) throws IOException {
        // 文件大小,以字节为单位
        int fileSize = 0;
        // 文件路径
        File filePath = new File("C:\\Users\\Admin\\Desktop\\fishing.jpg");

        // 从磁盘读取文件,并存入文件字节数组
        int readLen;
        byte[] buf = new byte[1024];
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
        System.out.println("文件读入中···");
        while ((readLen = bufferedInputStream.read(buf)) != -1) {
            // 将从文件读取到的二进制数据写进字节数组输出流(byteArrayOutputStream)
            byteArrayOutputStream.write(buf, 0, readLen);
            fileSize += readLen;
        }
        System.out.println("文件读入成功,文件大小为:" + fileSize + " bytes!");
        // 将得到的 byteArrayOutputStream 转换为一个字符数组
        byte[] fileByteArray = byteArrayOutputStream.toByteArray();

        // 将字节数组写入数据通道
        Socket socket = new Socket(InetAddress.getLocalHost(), 6666);
        OutputStream outputStream = socket.getOutputStream();
        System.out.println("文件发送到服务器中···");
        outputStream.write(fileByteArray);
        socket.shutdownOutput();
        System.out.println("文件发送到服务器成功!");

        // 接送服务器端发送的反馈信息并打印
        System.out.print("接收到的服务器信息:");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(bufferedReader.readLine());

        bufferedReader.close();
        outputStream.close();
        byteArrayOutputStream.close();
        bufferedInputStream.close();
        socket.close();
    }
}
  1. TCP 文件下载:
  • Server.java
/**
 * 服务端:将客户端请求的文件传输到客户端
 *
 * @author Spring-_-Bear
 * @version 2021-11-02 09:57
 */
public class Server {
    byte[] fileData = null;
    String requestFileName = null;
    ArrayList<String> fileNameList = new ArrayList<>();
    File filePath = new File("C:\\Users\\Admin\\Desktop\\sourceFile");

    Socket socket = null;
    ServerSocket serverSocket = null;
    InputStream inputStream = null;
    BufferedInputStream bufferedInputStream = null;

    public static void main(String[] args) {
        Server server = new Server();
        server.createAndReadFiles();
        server.startServer();
        server.receiveRequestNameFromClient();
        server.readFileFromDisk();
        server.transferFileToClient();
        try {
            server.socket.close();
            server.serverSocket.close();
            server.inputStream.close();
            server.bufferedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取指定路径下的所有文件
     */
    public void createAndReadFiles() {
        System.out.println("正在进行文件夹初始化···");
        // 若文件夹不存在,则先创建文件夹
        if (!(filePath.exists() && filePath.isDirectory())) {
            filePath.mkdirs();
        }

        // 读取文件夹中的所有文件,并将文件名存进集合
        File[] files = filePath.listFiles();
        for (File file : files) {
            if (file.isFile()) {
                fileNameList.add(file.getName());
            }
        }
        System.out.println("初始化完成!");
    }

    /**
     * 启动服务器
     */
    public void startServer() {
        try {
            serverSocket = new ServerSocket(9999);
            System.out.println("服务器已启动,等待连接···");
            socket = serverSocket.accept();
            System.out.println("服务端 - 客户端建立连接成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取客户端请求的文件名
     */
    public void receiveRequestNameFromClient() {
        try {
            int readLen;
            byte[] buf = new byte[2048];
            inputStream = socket.getInputStream();
            System.out.println("正在接收客户端请求的文件名···");
            while ((readLen = inputStream.read(buf)) != -1) {
                requestFileName = new String(buf, 0, readLen);
            }
            socket.shutdownInput();
            System.out.println("成功接收到客户端请求的文件名!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 从磁盘中读取文件内容到内存
     */
    public void readFileFromDisk() {
        try {
            int readLen = 0;
            byte[] buf = new byte[1024];
            File requestFile = new File(filePath + "\\" + requestFileName);
            bufferedInputStream = new BufferedInputStream(new FileInputStream(requestFile));
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            System.out.println("正在从磁盘读取文件···");
            while ((readLen = bufferedInputStream.read(buf)) != -1) {
                byteArrayOutputStream.write(buf, 0, readLen);
            }
            fileData = byteArrayOutputStream.toByteArray();
            System.out.println("从磁盘读取文件完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将文件字节数组传输到客户端
     */
    public void transferFileToClient() {
        try {
            System.out.println("正在传送文件到客户端···");
            OutputStream outputStream1 = socket.getOutputStream();
            outputStream1.write(fileData);
            System.out.println("文件传输到客户端完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    /**
//     * 发送文件中的文件夹中的所有文件名到客户端
//     */
//    public void sendFilesNameToClient() {
//        System.out.println("正在发送文件目录到客户端···");
//        outputStream = socket.getOutputStream();
//        bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
//        for (String fileName : fileArrayList) {
//            bufferedWriter.write((fileName));
//            outputStream.flush();
//        }
//        bufferedWriter.newLine();
//        outputStream.flush();
//        System.out.println("文件目录发送到客户端成功!");
//    }
}
  • Client.java
/**
 * 服务端:发送想要下载的文件名称到客户端,得到客户端回应并将文件保存到本地
 *
 * @author Spring-_-Bear
 * @version 2021-11-02 09:58
 */
public class Client {
    byte[] fileData = null;

    Socket socket = null;
    InputStream inputStream = null;
    OutputStream outputStream = null;
    BufferedOutputStream bufferedOutputStream = null;
    Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        Client client = new Client();

        try {
            client.socket = new Socket(InetAddress.getLocalHost(), 9999);
            client.sendFileNameToServer();
            client.receiveFileData();
            client.saveFile();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                client.socket.close();
                client.inputStream.close();
                client.outputStream.close();
                client.bufferedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送想要下载的文件名到服务器端
     */
    public void sendFileNameToServer() {
        try {
            System.out.print("请输入您想要下载的文件名(含文件拓展名):");
            String choice = scanner.next();
            outputStream = socket.getOutputStream();
            outputStream.write(choice.getBytes(StandardCharsets.UTF_8));
            socket.shutdownOutput();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接收从服务端传送的数据
     */
    public void receiveFileData() {
        try {
            int readLen = 0;
            byte[] buf = new byte[1024];
            inputStream = socket.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            System.out.println("正在接收服务器传输的文件···");
            while ((readLen = inputStream.read(buf)) != -1) {
                byteArrayOutputStream.write(buf, 0, readLen);
            }
            socket.shutdownInput();
            fileData = byteArrayOutputStream.toByteArray();
            System.out.println("文件接收成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将从服务器端接收到的文件保存到本地
     */
    public void saveFile() {
        try {
            bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("C:\\Users\\Admin\\Desktop\\temp.jpg"));
            System.out.println("正在将文件保存到本地···");
            bufferedOutputStream.write(fileData);
            System.out.println("文件保存到本地完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    /**
//     * 读取服务端发送的文件名并显示到控制台
//     */
//    public void receiveFilesName() {
//        bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//        System.out.println("正在接收服务端发送的文件目录···");
//        String fileName = "";
//        System.out.println(bufferedReader.readLine());
//        while ((fileName = bufferedReader.readLine()) != null) {
//            System.out.println(fileName);
//        }
//        System.out.println("接收服务端发送的文件目录成功!");
//    }
}

9.6 UDP 编程

UDP 数据报通过数据报套接字 DatagramSocket 实现发送和接收,系统不保证 UDP 数据报一定送达目的地,也不确定什么时候可以送达即传输不可靠。DatagramSocket 对象封装了 UDP 数据报,在数据报中封装了数据、发送端和接收端的 IP 地址以及端口信息

  • Sender.java
/**
 * @author Spring-_-Bear
 * @datetime 2022-09-25 23:02 Sunday
 */
public class Sender {
    public static void main(String[] args) throws IOException {
        // 监听端口
        DatagramSocket datagramSocket = new DatagramSocket(8888);

        // 装包:封装数据
        byte[] data = "Hello, receiver!".getBytes(StandardCharsets.UTF_8);
        DatagramPacket packet = new DatagramPacket(data, 0, data.length, InetAddress.getLocalHost(), 7777);
        // 发送包
        datagramSocket.send(packet);

        byte[] buf = new byte[1024];
        packet = new DatagramPacket(buf, buf.length);
        // 接收包并从包中获取数据
        datagramSocket.receive(packet);
        int dataLen = packet.getLength();
        data = packet.getData();
        System.out.println(new String(data, 0, dataLen));

        // 关闭资源
        datagramSocket.close();
    }
}
  • Receiver.java
/**
 * @author Spring-_-Bear
 * @datetime 2022-09-25 23:02 Sunday
 */
public class Receiver {
    public static void main(String[] args) throws IOException {
        // 监听端口
        DatagramSocket socket = new DatagramSocket(7777);

        byte[] buf = new byte[1024];
        DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
        // 接收包并从包中获取数据
        socket.receive(datagramPacket);
        int dataLen = datagramPacket.getLength();
        byte[] data = datagramPacket.getData();
        System.out.println(new String(data, 0, dataLen));

        // 装包:封装数据
        buf = "Hello, sender!".getBytes(StandardCharsets.UTF_8);
        datagramPacket = new DatagramPacket(buf, 0, buf.length, InetAddress.getLocalHost(), 8888);
        // 发送包
        socket.send(datagramPacket);

        // 关闭资源
        socket.close();
    }
}

9.7 netstat

  1. netstat -an | more:查看当前主机网络情况,包括端口监听和网络连接情况
  2. netstat -anb:以管理员身份运行此命令可以查看监听端口的具体程序

十、反射

10.1 反射机制

  1. 反射:允许程序在运行时借助于 Reflection API 取得类的任何内部信息(如成员变量、构造器、成员方法等),并能操作对象的属性及方法。反射在设计模式和框架底层广泛使用
  2. Class 对象:类加载完毕之后,在堆中就产生了一个 Class 类型(Class 本身也是一个类)的对象(一个类只有一个 Class 对象,因为类只加载一次),这个对象包含了类的完整结构信息,通过这个对象就可以得到类的所有信息
  3. 反射的作用:
  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象,获取其属性或调用其方法
  • 生成动态代理
  1. 反射的优缺点:
  • 优点:可以动态创建和使用对象(框架底层核心),没有反射框架就失去了灵魂
  • 缺点:使用反射代码基本是解释执行,对执行速度有一定影响
  • 优化:关闭访问检查,Method、Field、Constructor 类都有一个 setAccessible(boolean) 方法,作用是启动和禁用访问安全检查的开关,传入参数 true 表示反射的对象在使用时取消安全检查,提高反射效率,但提高效果并不明显
  1. 反射案例:
// 根据类名加载类信息
Class<?> aClass = Class.forName("cn.edu.whut.springbear.User");
// 创建类实例
Object o = aClass.newInstance();
// 获得类成员方法
Method method = aClass.getMethod("hello");
// 调用对象的方法
method.invoke(o);

10.2 程序三阶段

  1. 代码阶段(编译阶段):将源码 .java 文件通过 javac 编译得到 .class 字节码文件
  2. Class 类阶段(加载阶段):通过类加载器(ClassLoader)将 .class 字节码文件加载到堆中生成 Class 类的对象,该对象记录了 .class 中类的字段 Field[] fields、构造器 Constructor[] constructors、方法 Method[] methods 等信息
  3. Runtime(运行阶段):通过 new 创建出的对象知道自己与哪个 Class 类型的对象相关联

java的stu java的study_JDBC_15

10.3 Class

java的stu java的study_JDBC_16

  1. Class 对象:java.lang.Class 代表一个类,继承自 Object 类。Class 对象代表 某个类的字节码文件被类加载器加载后在堆中生成的 Class 类型的对象
  2. 类的元数据:加载类在堆里生成 Class 对象的同时也会在方法区生成对应类的 字节码的二进制数据,也称为类的元数据,该二进制数据引用到 Class 对象
  3. Class 对象的生成:Class 类对象并不是 new 出来的,而是 JVM 创建的,通过 ClassLoader 类的 loadClass 方法创建。对于某个类的 Class 对象,在堆中有且仅有一份,因为类只加载一次
  4. 哪些类型有 Class 对象:外部类、内部类、接口、数组、枚举、注解、基本数据类型、void
  5. Class 类常用 API

方法

功能

static Class forName(String)

获得指定类名的 Class 对象

Object newInstance()

调用缺省构造函数,获得 Class 类的一个实例

getFields

获取所有 public 修饰的字段,本类以及父类

getDeclaredFidles

获取本类中的所有字段

getMethods

获取所有 public 修饰的方法,本类以及父类

getDeclaredMethods

获取本类中的所有方法

getConstructors

获取所有 public 修饰的构造器,只包含本类

getDeclaredConstructors

获取本类中的所有构造器

getPackage

以 Package 形式返回包信息

getSuperClass

以 Class 形式返回父类信息

getAnnotations

以 Annotation[] 形式返回注解信息

Class[] getInterfaces

获得 Class 对象的接口

ClassLoder getClassLoder()

获得类的加载器

getName

获取全类名

getSimpleName

获取简单类名

  1. 获取类的 Class 对象:
  • 编译阶段:Class.forName(String); 多用于从配置文件读取到类全路径,而后加载类
  • 类加载阶段:ClassName.class; 多用于参数传递,此方式最为安全可靠,程序性能最高
  • 运行阶段:object.getClass(); 通过创建好的类的实例来获取 Class 对象
  • 类加载器:object.getClass().getClassLoader().loadClass("ClassFullPath");
Car car = new Car();
// 获得类对应的类加载器
ClassLoader classLoder = car.getClass().getClassLoader();
// 获得 Class 对象
Class<?> aClass = classLoder.loadClass("com.springbear.Car");
  • 基本数据类型:Class clazz = int.class;
  • 包装类:Class cls = Integer.TYPE;

10.4 类加载

  1. 静态加载与动态加载 :
  • 静态加载:编译时加载相关的类,如果不存在相关类则报错,依赖性很强
  • 动态加载:运行过程中在需要时加载该类,如果运行时未使用到该类则不报错,降低了依赖性。反射机制是 Java 实现动态的关键
  1. 引起类加载的情况:
  • new 创建对象时
  • 子类被加载时其父类也被加载
  • 访问到类的静态成员时(非 static final
  • 通过反射加载
  1. 类加载流程:加载 -> 连接 -> 初始化
  • 加载(Loading):类加载器完成将类的 .class 文件读入内存,并为之创建一个 Class 类型的对象
  • 连接(Linking):将类的二进制数据(类的元数据)合并到 JRE 中。细分为验证、准备和解析三个子阶段:
  • 验证(Verification):目的是为了确保 .class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害到虚拟机自身的安全。验证包括对文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证等。可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机加载时间
  • 准备(Preparation):JVM 会在此阶段给 静态变量 分配内存并执行默认初始化,这些变量所使用的内存都会在 方法区 中进行分配
// 在连接的准备子阶段先默认初始化为 0,后在初始化阶段再显式初始化为 1
public static int num1 = 1;
// 在连接的准备子阶段直接初始化为 2
public static final int num2 = 2;
  • 解析(Resolution):JVM 将常量池内的符号引用替换为直接引用的过程
  • 初始化(Initialization):真正执行类中定义的 Java 代码,此阶段是执行 <clinit>() 方法的过程。 <clinit>() 方法的执行过程是由编译器按代码在源文件中的出现顺序,依次收集类中所有的 静态变量的赋值动作和静态代码块中的语句,并自动进行合并。JVM 保证一个类的 <clinit>() 方法在多线程环境中会被正确地加锁、同步
static {
   System.out.println("静态代码块被执行!");
   num = 300;
}
public static int num = 100;

// 经 <clinit>() 方法收集合并后如下
System.out.println("静态代码块被执行!");
num = 100;
  1. 类加载后内存布局情况:
  • 在堆中存放类的 Class 对象(数据结构)
  • 在方法区存放类的字节码的二进制数据(元数据),元数据引用到对应的 Class 对象

java的stu java的study_Socket_17

10.5 反射爆破

  1. 调用类的指定构造器:
Class<?> userClass = Class.forName("com.springbear.User");
// 获得指定构造器
Constructor<?> userConstructor = userClass.getDeclaredConstructor(int.class, String.class);
// 爆破
userConstructor.setAccessible(true);
// 调用构造器
Object mary = userConstructor.newInstance(10, "mary");
System.out.println(mary);
  1. 操作类的字段:
Class<?> userClass = Class.forName("com.springbear.User");
Object userObject = userClass.newInstance();

// 获得指定字段
Field name = userClass.getDeclaredField("name");
// 爆破
name.setAccessible(true);
// 获得值
System.out.println(name.get(userObject));
// 设置值
name.set(userObject, "Spring-_-Bear");
System.out.println(name.get(userObject));
  1. 操作类的方法:
Class<?> userClass = Class.forName("com.springbear.User");
Object userObject = userClass.newInstance();

// 获取指定的方法
Method loginMethod = userClass.getDeclaredMethod("login", String.class, String.class);
// 爆破
loginMethod.setAccessible(true);
// 调用方法
Object springbear = loginMethod.invoke(userObject, "springbear", "123");
System.out.println(springbear);
  1. 最佳实践:在指定目录下创建文件
// 加载类信息
Class<?> fileClass = Class.forName("java.io.File");
// 获得指定的构造器
Constructor<?> constructor = fileClass.getConstructor(String.class);
Object file = constructor.newInstance("c:/users/admin/desktop/reflection.txt");
// 获得指定方法
Method createNewFile = fileClass.getMethod("createNewFile");
// 调用方法
createNewFile.invoke(file);

十一、JDBC

11.1 JDBC 原理

  1. JDBC(Java Database Connectivity):JDBC 为访问不同的数据库定义了统一的接口,具体实现由各数据库厂商完成。Java 程序员使用 JDBC API 可以连接任何提供了 JDBC 驱动程序的数据库系统,从而完成对数据库的各种操作,、关的接口和类在 java.sql 和 javax.sql 包中
  2. JDBC 步骤:
  • 注册驱动:加载 Driver 类
  • 获取连接:获得 Connection 对象
  • 执行 SQL:获得 Statement 对象
  • 释放数据库连接

11.2 五种连接方式

  1. 静态加载驱动类:
Properties properties = new Properties();
properties.setProperty("user", "admin");
properties.setProperty("password", "admin");
/*
         * 加载驱动类: mysql5.* new com.mysql.jdbc.Driver()
         * 加载驱动类: mysql8.* new com.mysql.cj.jdbc.Driver()
         */
Driver driver = new com.mysql.cj.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
// 获得数据库连接
Connection connection = driver.connect(url, properties);
System.out.println(connection);
connection.close();
  1. 反射加载驱动类:
// 连接到的数据库:jdbc:mysql://主机IP地址:端口/db_name
String url = "jdbc:mysql://localhost:3306/temp";
// 设置用户名与密码
Properties properties = new Properties();
properties.setProperty("user", "springbear");
properties.setProperty("password", "123");

// 加载类信息
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
// 获得类实例
Driver driver = (Driver) aClass.newInstance();
// 获得连接
Connection connection = driver.connect(url, properties);
System.out.println(connection);
connection.close();
  1. 使用 DriverManager 手动注册驱动类:
// 加载类信息
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
// 获得类实例
Driver driver = (Driver) aClass.newInstance();

String url = "jdbc:mysql://localhost:3306/temp";
String user = "springbear";
String pwd = "123";

// 注册驱动
DriverManager.registerDriver(driver);
// 获得连接
Connection connection = DriverManager.getConnection(url, user, pwd);
System.out.println(connection);
connection.close();
  1. 使用 DriverManager 自动注册驱动类:
// 加载类信息,在加载 Driver 的过程中自动完成注册
Class.forName("com.mysql.jdbc.Driver");
/*
 * JDK 底层自动注册驱动类,交给 DriverManager 进行管理
 * static {
 *      try {
 *          DriverManager.registerDriver(new Driver());
 *      } catch (SQLException var1) {
 *          throw new RuntimeException("Can't register driver!");
 *      }
 * }
 */
String url = "jdbc:mysql://localhost:3306/temp";
String user = "springbear";
String pwd = "123";

// 获得连接
Connection connection = DriverManager.getConnection(url, user, pwd);
System.out.println(connection);
connection.close();
  1. 从配置文件读取配置信息:
# jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.username=admin
jdbc.password=admin
// 从配置文件中读取数据库信息
Properties properties = new Properties();
properties.load(new FileInputStream("jdbc.properties"));
String driver = properties.getProperty("jdbc.driver");
String url = properties.getProperty("jdbc.url");
String username = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");

/*
 * mysql-connector-java 驱动文件 5.1.6 版本之后无需手动获取驱动类,
 * 即省略 Class.forName(“com.mysql.jdbc.Driver”)  也可以获得数据库连接。
 * 原因:从 jdk5 以后使用了 jdbc4,会自动调用 jar 包下的 META-INF\services\java.sql.Driver 文本中的类名称进行注册
 */

// 加载类信息,自动注册驱动(可省略)
// Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println(connection);

11.3 ResultSet

ResultSet:表示从数据库读取到的数据表结果集。ResultSet 对象保持一个光标 指向当前的数据行。最初,光标位于第一行之前。其有一个 next 方法将光标移动到下一行,并且由于在 ResultSet 对象中没有更多行时返回 false,因此常用 while 循环来遍历结果集

// 从配置文件中读取数据库信息
Properties properties = new Properties();
properties.load(new FileInputStream("jdbc.properties"));
String user = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");
String driver = properties.getProperty("jdbc.driver");
String url = properties.getProperty("jdbc.url");

// 加载类信息,自动注册驱动(可省略)
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
// Sql 语句
String sql = "SELECT * from t_user";
Statement statement = connection.createStatement();
// 执行 SELECT 语句
ResultSet resultSet = statement.executeQuery(sql);
// 遍历结果集
while (resultSet.next()) {
    // column index begin with 1
    System.out.println(resultSet.getString(1));
    System.out.println(resultSet.getString(2));
}
statement.close();
connection.close();

11.4 SQL 注入

  1. SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入的数据中注入非法的 SQL 语句段或命令,恶意攻击数据库
/**
* @author Spring-_-Bear
* @version 2021-11-09 09:48
*/
public class SqlInjection {
   public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
       // 获取用户想要查询的用户名和密码
       // Input userName = 1' or
       // Input pwd = or '1' = '1
       Scanner scanner = new Scanner(System.in);
       System.out.print("Input the name that you want to query:");
       String userName = scanner.nextLine();
       System.out.print("Input the password that you want to query:");
       String pwd = scanner.nextLine();

       // 加载配置文件
       Properties properties = new Properties();
       properties.load(new FileInputStream("config\\temp.properties"));

       // 加载驱动类信息,自动注册驱动
       Class.forName(properties.getProperty("driver"));
       // 获得连接
       Connection connection = DriverManager.getConnection(properties.getProperty("url"), properties);
       Statement statement = connection.createStatement();

       String select = "SELECT * FROM admin WHERE name='" + userName + "' AND pwd= '" + pwd + "'";
       ResultSet resultSet = statement.executeQuery(select);
       while (resultSet.next()) {
           userName = resultSet.getString(1);
           pwd = resultSet.getString(2);
           System.out.println(userName + "\t" + pwd);
       }

       resultSet.close();
       statement.close();
       connection.close();
   }
}
  1. 在建立数据库连接之后,想要对数据库进行操作一般有以下三种方式:
  • Statement:存在 SQL 注入
  • PreparedStatement:预处理(解决 SQL 注入)
  • CallableStatement:用于执行数据库存储过程

11.5 PreparedStatement

PreparedStatement 执行的 SQL 语句中的参数用问号(?)来表示,调用 PreparedStatement 对象的 setXxx() 方法来设置这些参数。setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引,从 1 开始,第二个参数是设置 SQL 语句中的参数的值

String select = "SELECT * FROM admin WHERE name = ? AND pwd= ?";
// SQL 语句预处理
PreparedStatement preparedStatement = connection.prepareStatement(select);
// column index begin with 1
preparedStatement.setString(1, userName);
preparedStatement.setString(2, pwd);

// 执行 SQL 语句,得到结果集
ResultSet resultSet = preparedStatement.executeQuery();

接口名

方法名

功能

Connection

createStatement()

创建执行静态 SQL 语句的对象

createPreparedStatement(sql)

获得 SQL 语句预处理对象

Statement

executeUpdate(sql)

执行 DML 语句,返回受影响行数

executeQuery(sql)

执行 DQL 语句,返回结果集

execute(sql)

执行任意 SQL 语句,返回布尔值

PreparedStatement

executeUpdate(sql)

执行 DML 语句,返回受影响行数

executeQuery(sql)

执行 DQL 语句,返回结果集

execute(sql)

执行任意 SQL 语句,返回布尔值

setXxx(index,value)

设置 SQL 语句中的值

setObject(index,value)

设置 SQL 语句中的值

ResultSet

next()

向下移动一行,到表尾返回 false

previous()

向上移动一行,到表头返回 false

getXxx(index || colLabel)

获得指定列的值

getObject(index || colLabel)

获得指定列的值

11.6 JDBC 事务操作

  1. 自动提交事务:JDBC 程序中当一个 Connection 对象被创建后,默认情况下自动提交事务,即每执行一条 SQL 语句时,如果执行成功就会向数据库自动提交保存,不能进行回滚
  2. 开启事务:可以调用 Connection 接口的 setAutoCommit(false) 方法关闭自动提交事务,在所有的 SQL 语句都执行成功后调用 commit() 方法提交事务,在其中某个操作失败或出现异常时调用 rollback() 方法回滚事务
public void transactional() throws SQLException {
    String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
    String username = "admin";
    String password = "admin";

    Connection connection = DriverManager.getConnection(url, username, password);
    // 关闭自动提交即开启事务
    connection.setAutoCommit(false);
    PreparedStatement preparedStatement = null;

    try {
        String add = "UPDATE account SET balance = balance + 100 WHERE id = 2";
        preparedStatement = connection.prepareStatement(add);
        preparedStatement.executeUpdate();
        int tmp = 1 / 0;
        String sub = "UPDATE account SET balance = balance - 100 WHERE id = 1";
        preparedStatement = connection.prepareStatement(sub);
        preparedStatement.executeUpdate();
        // 提交事务
        connection.commit();
    } catch (ArithmeticException e) {
        e.printStackTrace();
        // 发生异常,撤销操作,事务回滚
        connection.rollback();
    } finally {
        connection.close();
        Objects.requireNonNull(preparedStatement).close();
    }
}

11.7 批处理

  1. 批处理机制:当需要批量插入或者更新记录时,可以采用 Java 的批处理机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单条语句提交处理效率更高。若需开启批处理功能则需要在 url 后加入 rewriteBatchedStatements=true
jdbc.url=jdbc:mysql://localhost:3306/temp?rewriteBatchedStatements=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
  1. 批处理往往和 PreparedStatement 一起搭配使用,既可以减少编译次数又可以减少运行次数,效率大大提高
String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
String username = "admin";
String password = "admin";

Connection connection = DriverManager.getConnection(url, username, password);
String sql = "insert into t_user (username, password) values (?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 1; i <= 1000; i++) {
    preparedStatement.setString(1, "batch " + i);
    preparedStatement.setString(2, String.valueOf(i));
    preparedStatement.addBatch();
    if (i % 50 == 0) {
        // 批量发送执行
        preparedStatement.executeBatch();
        // 清空批处理包
        preparedStatement.clearBatch();
    }
}

preparedStatement.close();
connection.close();
  1. 批处理源码剖析:
/**
* 第一次会创建 ArrayList<elementData> 会存放预处理后的 SQL 语句,当 elementDate 满后会按照 1.5 倍扩容
* 当达到指定的容量之后,就会发送给 MySQL 执行,批处理会减少发送 SQL 语句的网络开销,减少编译次数,从而提高效率
*/
public void addBatch() throws SQLException {
   synchronized(this.checkClosed().getConnectionMutex()) {
       if (this.batchedArgs == null) {
           this.batchedArgs = new ArrayList();
       }

       for(int i = 0; i < this.parameterValues.length; ++i) {
           this.checkAllParametersSet(this.parameterValues[i], this.parameterStreams[i], i);
       }

       this.batchedArgs.add(new com.mysql.jdbc.PreparedStatement.BatchParams(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths, this.isNull));
   }
}

11.8 数据库连接池

  1. 传统 JDBC 的弊端:
  • 传统的 JDBC 数据库连接使用 DriverManager 来获取,每次建立数据库连接的时都需要将 Connection 加载到内存中,再验证 IP 地址、用户名、密码(耗时0.05s ~ 1s)是否正确
  • 当需要连接数据库时就向数据库请求一个连接,频繁的请求操作将占用过多的系统资源,容易造成服务器崩溃
  • 每一次数据库连接使用完后都得及时释放,如果程序出现异常导致未能正常关闭数据库连接,过多的数据库连接将导致数据库内存泄漏,最终导致数据库崩溃
  1. 数据库连接池原理:预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从 “缓冲池” 中取出一个,使用完毕归还给缓冲池(并不断开与数据库的连接)。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个,大大减轻了数据库的压力
  2. DataSource:JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,具体实现留给第三方

连接池

特点

C3P0

速度相对较慢,稳定性不错(Hibernate、Spring 框架底层均采用)

Druid

阿里巴巴提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身

Proxool

有监控连接池状态的功能,稳定性较 C3P0 略差

BoneCP

速度快

DBCP

速度较 C3P0 快,但不稳定

  1. C3P0 数据库连接池:
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>c3p0</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.1.2</version>
</dependency>
  • 方式一:jdbc.properties
// 创建数据源对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 设置相关信息
comboPooledDataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/temp");
comboPooledDataSource.setUser("admin");
comboPooledDataSource.setPassword("admin");
comboPooledDataSource.setInitialPoolSize(10);
comboPooledDataSource.setMaxPoolSize(50);
// 获取一个数据库连接
Connection connection = comboPooledDataSource.getConnection();
System.out.println(connection);
connection.close();
  • 方式二:c3p0-config.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
        <property name="jdbcUrl"><![CDATA[jdbc:mysql://localhost:3306/temp?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8]]></property>
        <property name="user">admin</property>
        <property name="password">admin</property>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
    </default-config>
</c3p0-config>
// 将数据库配置信息写入到 c3p0-config.xml 配置文件中,并将其拷贝到类路径下,C3P0 将会自动加载并读取此配置文件
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
Connection connection = comboPooledDataSource.getConnection();
System.out.println(connection);
connection.close();
  1. Druid 数据库连接池:
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>
# druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/temp?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
characterEncoding=utf-8
username=admin
password=admin
initialSize=5
maxActive=10
maxWait=3000
validationQuery=SELECT 1
testWhileIdle=true
// 加载配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("druid.properties"));
// 创建一个的连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();

11.9 DBUtils

  1. ResultSet 存在的问题:关闭 connection 后,resultSet 结果集无法继续使用,然而很多时候我们希望关闭 connection 连接后仍然可以继续使用查询到的数据;且 resultSet 存储查询结果的方式不利于数据管理,从 resultSet 结果集中获取数据时操作方法不够明确,getXxx() 方法容易出错,含义模糊
  2. JavaBean:定义一个类与数据库表的字段一一对应,这样的类一般称作 JavaBean 或 Domain 或 POJO 或 Entity,将返回的结果集的字段值封装到自定义的类的对象中,将若干个这样的对象放进集合中,就可以直接访问集合从而获得数据库表的查询结果
  • 在创建 JavaBean 时类的字段的数据类型强制使用八大基本数据类型对应的包装类,因为 MySQL 数据库表中的字段值可能为空,而 Java 只有引用数据类型才有 NULL 值。创建 JavaBean 类的时候一定要给一个无参构造器和对应的 getter、setter 方法,以方便通过反射机制获取该类信息
  1. commons-dbutils:commons-dbutils 是 Apache 组织提供的一个开源 JDBC 工具类,它是对 JDBC 的封装,使用 dbutils 能极大简化 JDBC 编码量
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>commons-dbutils</groupId>
    <artifactId>commons-dbutils</artifactId>
    <version>1.3</version>
</dependency>
// 获得连接
Connection connection = DruidUtils.getConnection();
// 获得 Apache 实现的查询对象
QueryRunner queryRunner = new QueryRunner();
String select = "SELECT * FROM cost WHERE id >= ? AND id <= ?";
// 执行 SQL 语句获取查询结果
List<Fishing> fishings = queryRunner.query(connection, select, new BeanListHandler<>(Fishing.class), 1, 10);
for (Fishing fishing : fishings) {
    System.out.println(fishing);
}
connection.close();

方法

功能

ArrayHandler

将结果集中的第一行数据转换成对象数组

ArrayListHandler

将结果集中的每一行都转换成一个数组,再存放到 List 中

BeanHandler

将结果集中的第一行数据封装到一个对应的 JavaBean 实例中

BeanListHandler

将结果集中的每一行数据封装到对应的 JavaBean 实例中,存放到 List

ColumnListHandler

将结果集中的某一列数据存放到 List 中

KeyedHandler(name)

将每行数据封装到 Map 中,再将 map 存入另一个 Map 中

MapHandler

将结果集的第一行数据封装到 Map 中,key 是列名,value 是对应值

MapListHandler

将结果集中的每一行数据封装到 Map 中,再存放到 List 里

  1. BaseDao:为每张数据库表设计一个 JavaBean ,同时为每一张数据库表设计一个专门操作该表的数据访问对象 Dao(Data Access Object),将所有的具体 Dao 类中的共有部分抽象出父类 BaseDao,以更好地利用多态完成数据库操作
/**
 * @author Spring-_-Bear
 * @datetime 2022/3/16 18:54
 */
public abstract class BaseDao {
    private final QueryRunner queryRunner = new QueryRunner();

    /**
     * 执行 insert、update、delete 语句
     *
     * @param sql    sql
     * @param params sql 实参
     * @return 受影响的行数
     */
    public int update(String sql, Object... params) {
        Connection connection = null;
        try {
            connection = JdbcUtil.getConnection();
            return queryRunner.update(connection, sql, params);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection);
        }
        return -1;
    }

    /**
     * 查询数据库表的一条记录
     *
     * @param clazz  JavaBean class 对象
     * @param sql    sql
     * @param params sql 实参
     * @param <T>    返回类型泛型
     * @return 一条记录 or null
     */
    public <T> T getRecord(Class<T> clazz, String sql, Object... params) {
        Connection connection = null;
        try {
            connection = JdbcUtil.getConnection();
            return queryRunner.query(connection, sql, new BeanHandler<>(clazz), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection);
        }
        return null;
    }

    /**
     * 查询返回多条数据库表记录
     *
     * @param clazz  JavaBean 的 class 对象
     * @param sql    sql
     * @param params sql 实参
     * @param <T>    返回类型的泛型
     * @return 多条记录 or null
     */
    public <T> List<T> listRecord(Class<T> clazz, String sql, Object... params) {
        Connection connection = null;
        try {
            connection = JdbcUtil.getConnection();
            return queryRunner.query(connection, sql, new BeanListHandler<>(clazz), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection);
        }
        return null;
    }

    /**
     * 查询返回单个数值
     *
     * @param sql    sql
     * @param params sql 实参
     * @return 单个数值 or null
     */
    public Object getSingleValue(String sql, Object... params) {
        Connection connection = null;
        try {
            connection = JdbcUtil.getConnection();
            return queryRunner.query(connection, sql, new ScalarHandler(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection);
        }
        return null;
    }
}