Java从入门到放弃

自身感觉学的体系日渐庞大,虽然可以渐渐的熟悉项目开发,但是自身知识消化理解还不是很好。遂准备重新复习基础。

基础

变量

变量:在内存里开辟一个合适的空间来储存数据

特性: 1.)存储数据 2.)允许被改变 3.)系统来分配,在内存里开辟一个内存空间,
先声明,再赋值,使用

数据类型

数据过大溢出

随机数

random 产生的是[0,1)的小数。
如果想要产生[0–50)之间的数字: random() * 50
如果想要产生[50–100]之间的整数: (int)Math.random() * 50 + 50

取余(rem)取整 取模(Mod)

  • 取余 %:

    10 % 3 = 1(10 除以 3)—-求余数
    2 % 5 = 2 (没有可以整除 5 那部分的)
    rem(3,2) = 1
    rem(-3,-2) = -1
    rem(3,-2) = 1
    rem(-3,2) = -1

  • 取模运算:Math.floorMod()

    mod 结果的符号与除数 b 相同
    mod(3,2)=1
    mod(-3,-2)=-1
    mod(3,-2)=-1
    mod(-3,2)=1

  • 取整

    5 / 2 = 2 (2.5)
    2 / 5 = 0 (0.4)

注意:
当除数与被除数的符号相同时,rem 和 mod 的结果是完全相同的;
当除数与被除数的符号不相同时,结果不同;

自加自减运算

不过先加减还是先运算再加减,值都是变化了

加加 减减 运算后 值都是会改变的;
只是 a++是值改变但是对本身的式子不会产生影响(因为已经运算完了,须传递给后面的式子)
而 ++a 是 先 自加 然后再进行本身式子的运算,所以会立即产生影响

三元运算符

判断条件 ? 表达式 1 : 表达式 2

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 求一个数的四舍五入;
* num1 = 3;
* num2 = 0.5099999999999998;
* result = 4;
*/
package xnxy.blue.ternary.operator;

public class TernaryOperator {
public static void main(String[] args) {
//求一个数的四舍五入;
double num = 3.51;
//这个数的整数部分;
int num1 = (int)num;
//这个数的小数部分
double num2 = num - num1;
//运用三元运算符四舍五入,再把 double类型 的强制转换成了 int类型;
int result = (int) (num2 >= 0.5 ? num1+1 : num1);
System.out.println("num1="+num1+",num2="+num2);
System.out.println(result);
}
}

异或


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.blue.logicaloperator;

public class LogicalOperatorSecondDemo {
public static void main(String[] args) {
System.out.println(true | true);
System.out.println(true | false);
System.out.println(true || (1 / 0 == 0));//没有报错
//System.out.println(true | (1 / 0 == 0));//报错,运算1/0为无穷大

//非:相反
System.out.println("--------------");
System.out.println(!false);//true
System.out.println(!true);//false
System.out.println(!!true);//true

System.out.println("--------------");
//异或:相同则为false ; 不同则为 true
System.out.println(false ^ false);//false
System.out.println(true ^ true);//false
System.out.println(false ^ true);//true
}
}

两个变量间值的交换

键盘扫描器 Scanner

1
2
3
4
5
Scanner input = new Scanner(System.in)

int num1 = input.nextint; //(next 行 + 类型)
char c1 = input.next().charAt(0);//字符型
String str = input.next();//字符串类型

switch 语句

1
2
3
4
5
6
7
8
9
switch(整数选择因子或者字符串或者枚举) {
  case 整数值 1 : 语句; break;
  case 整数值 2 : 语句; break;
  case 整数值 3 : 语句; break;
  case 整数值 4 : 语句; break;
  case 整数值 5 : 语句; break;
  //..
  default:语句;92
}

switch 语句接受的数据类型

switch 语句中的表达式的数据类型,是有要求的
JDK1.0 - 1.4 数据类型接受 byte short int char
JDK1.5 数据类型接受 byte short int char enum(枚举)
JDK1.7 数据类型接受 byte short int char enum(枚举), String

循环语句

  • 循环语句 for
    for 循环语句是最常用的循环语句,一般用在循环次数已知的情况下。for 循环语句的语法格式如下:
1
2
3
4
5
for(初始化表达式; 循环条件; 操作表达式){

执行语句
………
}

接下来分别用 ① 表示初始化表达式、② 表示循环条件、③ 表示操作表达式、④ 表示循环体,通过序号来具体分析 for 循环的执行流程。具体如下:
for(① ; ② ; ③){
   ④
}

  • 第一步,执行 ①

  • 第二步,执行 ②,如果判断结果为 true,执行第三步,如果判断结果为 false,执行第五步

  • 第三步,执行 ④

  • 第四步,执行 ③,然后重复执行第二步

  • 第五步,退出循环

  • do…while 语句
    特点:先执行一次循环体,在判断表达式,若为 true 就执行循环体,否则,跳过循环体,也就是先执行再判断,不管怎么都会至少执行一次

    do…while 循环语句和 while 循环语句功能类似,其语法结构如下:
    do {

    执行语句
    ………

    } while(循环条件);

跳转语句(break、continue)

  • break 语句

    在 switch 条件语句和循环语句中都可以使用 break 语句。当它出现在 switch 条件语句中时,作用是终止某个 case 并跳出 switch 结构。当它出现在循环语句中,作用是跳出循环语句,执行后面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
public class BreakDemo {
public static void main(String[] args) {
int x = 1; // 定义变量x,初始值为1
while (x <= 4) { // 循环条件
System.out.println("x = " + x); // 条件成立,打印x的值
if (x == 3) {
break;
}
x++; // x进行自增
}
}
}
  • 使用标记跳出循环

    当 break 语句出现在嵌套循环中的内层循环时,它只能跳出内层循环,如果想使用 break 语句跳出外层循环则需要对外层循环添加标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BreakDemo02 {
public static void main(String[] args) {
int i, j; // 定义两个循环变量
Loop: for (i = 1; i <= 9; i++) { // 外层循环
for (j = 1; j <= i; j++) { // 内层循环
if (i > 4) { // 判断i的值是否大于4
break Loop; // 跳出外层循环
}
System.out.print("*"); // 打印*
}
System.out.print("\n"); // 换行
}
}
}
  • continue 语句

    continue 语句用在循环语句中,它的作用是终止本次循环,执行下一次循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ContinueDemo {
public static void main(String[] args) {
int sum = 0; // 定义变量sum,用于记住和
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) { // i是一个偶数,不累加
continue; // 结束本次循环
}
sum += i; // 实现sum和i的累加
}
System.out.println("sum = " + sum);
}
}
//sum = 2500

九九乘法口诀

数组

变量空间:是在栈里面开辟的并且地址是由系统分配的
数组的开辟:是在堆里面开辟一块连续的内存空间,并且这个内存空间的大小是可以由程序控制的(在栈里面分配)

数组的特性:

  • 空间连续(知道第一个地址,就知道第二个第三个……)
  • 长度固定(局限性)
  • 类型单一(开辟的整型的数组,就只能存放整型的,不过 byte,short 也可 以)因为可以自动转换

数组的定义:

  • 静态初始化:
    • 数据类型 [ ] 数组名 = {值列表}; * 数据类型 [ ] 数组名 = new 数据类型[ ]{值列表};
    • 在数据已知的时候使用
  • 动态初始化:
    • 数据类型 [ ] 数组名 = new 数据类型[ 数组长度] - 数组赋值:数组名[索引下标 ] = 值; - 属性名.length

二维数组:

静态: 数据类型 数组名 [][] = {值列表 1},{值列表 2},{值列表 3}…. 记得大花括号都括起来(语法问题报错)
动态: 数据类型 数组名 [][] = new 数据类型[ 数组长度 ][ 数组长度]

排序

  • 升序排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//给数组升序排序
int[] num = {28,25,5,8,13} ;
//外层循环:比较的轮数
for(int i = 0;i < num.length - 1;i++) {
//里层循环:每一轮比较的次数
for(int j = 0;j < num.length - 1 - i;j++) {
//比较交换
if(num[j] > num[j + 1]) {
//定义临时变量
int temp = num[j + 1];
num[j + 1] = num[j];
num[j] = temp;
}
}
}
//打印升序后的数组
for(int i = 0;i < num.length;i++) {
System.out.println(num[i] + " ");
}

//输出:5 8 13 25 28
  • 降序排序
1
2
3
4
5
6
7
8
int[] num2 = new int[原来数组 num.length];
for(int index = num2.length - 1;index >= 0;index--) {
num2[num2.length - index - 1] = num2[index];
}
//System.out.print("数组降序排序:");
for(int index = 0;index < num2.length;index++) {
System.out.print(num2[index] + " ");
}
  • 选择算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
int[] num = {28,25,3,21,14};
for(int i = 0;i < num.length - 1;i++) {
for(int j = i + 1;j < num.length;j++) {
//num[i] = num[j - 1]
if(num[i] > num[j]) {
int temp = num[i];
num[i] = num[j];
num[j] = temp;
}
}
}
for(int i = 0;i < num.length;i++) {
System.out.print(num[i] + " ");
}
}


// 输出: 3 14 21 25 28
// 注意索引下标越界
  • 插入算法
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
package com.summarize;

import java.util.Scanner;

public class test1 {
public static void main(String[] args) {
int[] num1 = {3,10,15,23,0};
//int[] num1 = {3,10,15,23,}; //插入25 -->3 10 15 25;因为数组长度为 4. //第五个没有数 故不占内存
Scanner input = new Scanner(System.in);
System.out.println("请输入要插入的值:");
int n = input.nextInt();
// 输入一个值,插入数组中,使数组能够升序排序

// 1.先找到插入的索引下标
int index = num1.length - 1; //解决插入的数比目前的都大的问题
for(int m = 0;m < num1.length;m++) {
// 如果有比它大的,就把值赋值给比它大的那个数对应的索引
// 如果没有比它大的,就直接把原始值 num1.length - 1 赋值给index
if(num1[m] > n) {
index = m;
break;
}
}
//2.后面的数据后移
for(int m = num1.length - 1;m > index;m--) {
num1[m] = num1[m - 1];
}
//3.把数据插入到索引
num1[index] = n;
System.out.println("插入后的结果:");
for(int m = 0;m < num1.length;m++) {
System.out.print(num1[m] + " ");
}
}
}
  • 二分法
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
package cn.blue.homework;

import java.util.Scanner;
//二维数组的不规则打印,需要用到 数组名[i:第几轮].length.
public class test {
/*
*输入五个数
*升序
*二分法查找数据
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int[] num = new int[5];
for(int i = 0;i < 5;i++) {
System.out.println("请输入数组的第" + i + "个数:" );
num[i] = input.nextInt();
}
System.out.println();
System.out.print("输出的数组是:");
for(int i = 0;i < 5;i++) {
System.out.print(num[i] + " ");
}
//将输出的数组升序排序
for(int i = 0;i < num.length - 1;i++) {
System.out.println("");
for(int j = 0;j < num.length - i - 1;j++) {
if(num[j] > num[j + 1]) {
int temp = num[j + 1];
num[j + 1] = num[j];
num[j] = temp;
}
}
}
for(int i = 0;i < num.length;i++) {
System.out.println(num[i] + " ");
}
//普通方法查找数据
System.out.print("请输入你要查找的数字:");
int number = input.nextInt();
/*
for(int i = 0;i < num.length;i++) {
if(num[i] == number) {
System.out.print("该数字在第" + (i+1) + "个");
}else {
continue;
}
*/
//二分法查找 + for
int low = 0;
int high = num.length - 1;
/*
for(int i = 0;i <= high;i++) {
int middle = (high + low) / 2;
if(number == num[i]) {
System.out.print("该数字在第" + (i+1) + "个");
}else if(number < num[i]) {
high = middle;
}else {
low = middle;
}
}
*/
int index = binarySearch(number,4); 报错
}
//二分法查找 + while(要使用方法,添加int)
static int binaryArray(int num[],int number) {
int low = 0;
int high = num.length - 1;
while(low <= high) {
int middle = (high + low) / 2;
if(number == num[middle]) {
return middle;
}else if(number < num[middle]) {
high = middle;
}else {
low = middle;
}
}
return - 1;
}
}

方法

特点:方法的单一性,一个方法实现一个功能

  • 方法的执行流程

  • 方法重载设计

    • 特点
      方法名一致
      参数不一样(类型,个数,顺序)

类和对象

  • 类:

    抽象的,具有共同行为特征的一组对象的集合(对象的模板)
    在一组对象里抽取共同的行为特征

  • 对象

    实际存在的,看得见摸得到,并且不随主观意识的改变而改变(类的实例)

  • 属性

    对象具有的各种特征
    每个对象的每个属性都拥有特征值

方法

对象的行为

  • 构造方法

    作用:
    创建对象并且初始化对象
    对象出生的方法
    特点:
    方法名与类名一致
    无返回值,但 void(无返回值类型,故构造方法里没有 void)
    不写,虚拟机会默认生成一个无参数的构造方法
    如果有显示的构造方法就不会生成

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
public class Customer {
/*
描述类的属性:
成员变量:类的里面,方法的外面 ,有默认值
this.变量 = 成员变量;
this: 表示的是当前操作的对象
*/
//姓名
public String name;
//年龄
public int age;

//体重
public int weight;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setWeight(int Weight) {
this.weight = weight;
}
public int getWeight() {
return weight;
}
public Customer(String name,int age,int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}

}

new 对象:Customer zhanghao = new Customer(“张浩”,20,60);

  • 一个对象的内存图

关键字

  • this 关键字

    使用:当局部变量与成员变量名重名,如果要调用成员变量,此时在变量前加 this 关键字
    意义:代表着当前操作的对象
    原理图:

  • static 关键字

    使用:

    当希望某个成员变量属于整个类而不是某个对象,就加static
    eg:public static int count; //学生人数的累加

    意义:
    static 修饰的成员,此成员属于某个类
    总结:
    1.)用 static 修饰的成员,不能用 this 关键字
    2.)实例方法(非静态的方法)可以直接使用静态成员
    3.)静态方法不能直接调用实例对象(非静态的成员)
    4.)静态成员:类名.实例对象
    不能直接调用,可以创建新对象
    5.)static 不能修饰构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StudentDemo {
public int age;
//学生人数
public static int count;
//重写StudentDemo方法
public StudentDemo() {
count++;
}
public void print() {
System.out.println("学生人数:"+count);
}
}

public class StudentCount {
public static void main(String[] args) {
//每创建一个对象就调用一次无参构造,count就可以加一
StudentDemo student1 = new StudentDemo();
student1 .print(); // 1
StudentDemo student2 = new StudentDemo();
student2 .print(); // 2
}
}

  • final 关键字
    继承关系最大弊端是破环封装:子类能访问父类的实现细节,而且可以通过方法覆盖的形式修改实现细节
    含义:最终的,不可改变的,它可以修饰非抽象类,非抽象方法和变量。注意:构造方法不能使用 final 修饰,因为本身不能被继承,肯定是最终的。

    final修饰的变量称为常量,这些变量只能赋值一次。

    1
    2
    final int i = 20;
    i = 30; //赋值报错,final修饰的变量只能赋值一次

    引用类型的变量值为对象地址值,地址值不能更改,但是地址内的对象属性值可以修改。

    1
    2
    3
    4
    5
    final Person p = new Person();
    Person p2 = new Person();
    p = p2; //final修饰的变量p,所记录的地址值不能改变
    p.name = "小明";//可以更改p对象中name属性值
    //p不能为别的对象,而p对象中的name或age属性值可更改。

    修饰成员变量,需要在创建对象前赋值,否则报错。(当没有显式赋值时,多个构造方法的均需要为其赋值。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Demo {
    //直接赋值
    final int m = 100;

    //final修饰的成员变量,需要在创建对象前赋值,否则报错。
    final int n;
    public Demo(){
    //可以在创建对象时所调用的构造方法中,为变量n赋值
    n = 2016;
    }
    }

  • super 关键字
    当前对象的父类对象

    因为通过无参构造器,我们就可以点出方法

{ } 代码块

执行时机:类加载之后,对象创建之前(对象创建一次就执行一次)
执行过程:
    1.) 加载类
    2.) 创建对象
    3.) 构造方法执行(初始化的作用)

静态代码块:
执行时机:类加载时(只执行了一次)
static {
System.out.print(“”);
}

封装

概念:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问
前提:私有的 private
目的:解决安全性的问题
步骤:

1. 把属性设置为 私有                                     ---->设置为 private
2. 创建共有的setter/getter方法                     ---->便于属性的读写
3. 在setter/getter方法中加入属性控制语句    ---->对属性的合法性进行判断

继承 extends

语法:子类 extends 父类
特点: 1.)从内存角度讲:继承父类所有成员(成员变量,成员方法) 2.)继承单向性:
子类可以调用父类成员
父类不能调用子类成员 3.)应用角度讲:private 私有的,虽然继承过来了,但是我只是没有访问权限 4.)继承单一性:
只能继承一个父类,不能继承多个
一个父类可以有多个子类 5.)继承传递性:
父类可以传递给子类,子类再传递给子类的子类(儿子也继承爷爷的) 6.)构造方法不能被继承的
继承中的构造方法:
子类对象产生的时候,会优先创建父类对象
解决:代码重用性的问题

  • 继承图

重写(方法覆盖)

使用:当父类的方法无法满足子类的需求,重写父类的方法
特点:

  1. 方法名一致,参数一致,返回类型一致。
  2. 访问权限大于或者等于父类方法的访问权限
  3. 方法体不相同
  4. 使用注解@Override

实现方式:

  1. 继承父类
  2. 实现接口

方法重写和方法重载的区别

修饰符的访问权限

修饰符 | 同类 | 同包 | 子类 | 其他包类 | 备注
:-: | :-: | :-: | :-: | :-: | :-: | :-:
public | true | true | true | true | 访问权限最高
protected | true | true | true | false |
default | true | true | false | false | 不写就是默认的
private | true | false | false | false | 访问权限最低,只能在本类中访问

抽象类 abstract

使用:当某个类不希望被实例化,将此类设为抽象类(可以 new,但不能被实例化)

修饰类:class 前面 + abstract
修饰方法:访问修饰符 + abstract + [返回值类型]

eg:public abstract void eat();

抽象方法:
1.)抽象方法没有方法体{ }
2.)访问权限不能是 private
3.)抽象方法实现靠子类继承然后重写实现
4.)子类必须实现父类的抽象方法,如果不实现此类必然也要定义为抽象类(只要有一个抽象的方法,就算不使用,也要定义为抽象类)

原因:
1.)抽象类里面可以有实例方法也可以有抽象方法
2.)实例类里面不能有抽象方法,一旦某个类有抽象方法此类必须定义为抽象类

多态

特点:

1.引用转型:父类引用指向了子类对象

2.方法覆盖:子类覆盖父类的方法

实现多态的两种形式:

使用父类作为方法形参实现多态: public void play(Pet p){}
使用父类作为方法返回值实现多态: public Pet getPet(int type){}

多态理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
public void say(){
System.out.println("Person.say");
}
}
public class Man extends Person{
public void say(){
System.out.println("Man.say");
}
}
public class Women extend Person{
public void say(){
System.out.println("Women.say");
}
}

Person person = new Man();//向上转型(把某个对象的引用视为对其基类型的引用),向上转型会缩小接口范围

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
Test.operate(new Man());
Test.operate(new Women());//man对象和women向上转型为person
}
public static void operate(Person person) {
person.say();//动态绑定,调用对应的say
}
}

接口

实现接口:

[访问权限] interface 接口名称{
//规范
}

注意:

  • 一般来说会使用 I 开头 + 名称
  • 成员变量:
    • 默认的是公共的静态常量 public final static:public final static int age = 20;
    • 静止的属于类,不能被重写
  • 成员方法:
    • 公共的抽象方法 public abstract:public abstract void show();
    • jdk1.7 后的: public abstract void show();
  • 接口的方法,实现此接口的类必须实现接口的所有方法;

实现:

  • 类名 implements 接口列表(Ilock ,ITv)
  • 接口可以多重实现
  • 接口不能被实例化

接口类与抽象类的区别:

接口的特点和接的继承:

匿名对象

匿名对象是指创建对象时,只有创建对象的语句,却没有把对象地址值赋值给某个变量。

1
2
3
4
5
6
7
8
9
10
public class Person{
public void eat(){
System.out.println();
}
}

//创建一个普通对象
Person p = new Person();
//创建一个匿名对象
new Person();

匿名对象的特点:
创建匿名对象直接使用,没有变量名。
new Person().eat() //eat方法被一个没有名字的Person对象调用了。

类与类之间的关系

  • 内部类
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 Employee {
//成员变量
private String name;

private Employee() {

}

//成员方法
public void work() {
System.out.println(name + "员工工作!");
}

//内部类
class Phone {
private String name;
public void show(Employee el) {
//调用的是Employee里的成员变量
System.out.println(el.name);
//调用的是Employee里内部Phone的成员变量
System.out.println(this.name);
}
}
}
  • 静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
static class Phone {
public String name;
public void show(Employee el) {
System.out.println(el.name);
System.out.println(this.name);
}
}

=========================
//由于Phone是静态的,所以需要用Employee.Phone来new Phone
Employee.Phone pr = new Employee.Phone();
pr.name = "李四";
  • 匿名内部类
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
public class Test {
/*匿名内部类:
可以使用外部成员变量
匿名 实现了某个类 在雇员内部 ; 缺点:用完即丢,只用一次*/
public void height() {
//new的抽象类的实现类(只有类里才有大括号 方法)
Door door = new Door(){

@Override
public void open() {
// TODO Auto-generated method stub
}

@Override
public void close() {
// TODO Auto-generated method stub
}

};
//new了一个类,此类继承了Student。
Student student = new Student() {
@Override
public void Study() {
System.out.println("学生在学习");
}
};

}

异常

异常:依靠程序能够解决的轻微问题
错误:依靠程序不能解决的严重问题

Exception:所有异常的父类
RuntimeException:运行时异常, 不要求程序必须做出处理
checked异常:程序必须处理的异常
ArithmeticException:算术异常
InputMismatchException:输入类型不匹配
ArrayIndexOutOfBoundsException :数组下标越界异常
NullPointerException:空指针异常
ClassNotFoundException:类无法加载异常
IllegalArgumentException:方法接收到非法参数
ClassCastException:对象强制类型转换出错
NumberFormatException:数字格式转换异常,如把”abc”转换成数字

Throwable 类是 Java 语言中所有错误或异常的超类。

  • try: 可能会引发异常的代码块
    • 功能:捕获异常
    • 里面引发异常的位置后面的代码不会执行
  • catch: 对应异常出现的解决方案
    • 可以有多个catch
    • 排序要从子到父
    • 有异常就要捕获,不管有return或System.exit(0);
  • finally
    • 不论引发异常与否,就算有return,这串代码都会被执行;
    • finally唯一不执行的情况,程序直接退出了 :System.exit(0);

字符串

String对象

特性:不可变性 长度不可变
字符串的分类:

可变的字符串:StringBuilder / StringBuffer
当对象创建完毕后,该对象的内容可以发生改变,当内容发生改变的时候,对象保持不变

字符串的本质(底层是什么):

底层其实是char[],char表示一个字符,数组表示同一种类型的多个数据如何理解char[]:
String str = “ABCDEFG”, //定义一个字符串对象 等价于
char[] cs = new char[]{‘A’,’B’,’C’,’D’,’E’,’F’,’G’};

注:这里不考虑常量池本身含有需要被创建的对象,如果有的话,理应少一个创建的对象
str2 字符串 + 常量 会在堆里面创建对象
常量 + 常量 就直接在常量池 创建”张三李四”
============================================
String str3 = “张三”+”李四”;
创建三个对象:张三 李四 张三李四
============================================
String str1 = “张三”;
String str2 =str1+”李四”;
创建四个对象:张三 李四 张三李四 堆里面一个对象空间
============================================
String str4 = new String(“张三”);
创建两个:张三 堆里面
============================================
JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即(str1 + “李四”)无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder连接后的新String对象赋给str2。

String的拼接:
因为String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容。

StringBuffer

StringBuffer:常用于频繁的修改,线程安全的,加锁(synchronized)
字符串反转(reverse):

1
2
3
4
5
6
7
8
9
public class StringBufferDemo04{
public static void main(String[] args){
StringBuffer buf = new StringBuffer() ; // 声明StringBuffer对象
buf.append("World!!") ; // 添加内容
buf.insert(0,"Hello ") ; // 在第一个内容之前添加内容
String str = buf.reverse().toString() ; // 将内容反转后变为String类型
System.out.println(str) ; // 将内容输出
}
}

打印内容:!!dlroW olleH
替换字符串指定内容

1
2
3
4
5
6
7
8
public class StringBufferDemo05{
public static void main(String[] args){
StringBuffer buf = new StringBuffer() ; // 声明StringBuffer对象
buf.append("Hello ").append("World!!") ; // 向StringBuffer添加内容
buf.replace(6, 11, "偶my耶") ; // 将world的内容替换
System.out.println("内容替换之后的结果:" + buf) ; // 输出内容
}
}

打印内容:Hello 偶my耶!!

StringBuilder

StringBuilder:线程不安全,效率最高,多用它拼接字符串

包装类

Byte short int long float double char boolean void 原始型
Byte Short Integer Long Float Double Character Boolean Void 包装类

原始型 | Byte | short | int | long | float | double | char | boolean | void
:-: | :-: | :-: | :-: | :-:| :-: | :-:| :-: | :-:| :-: | :-:
包装类 | Byte | Short | Integer | Long | float | Double | Char | Boolean | Void

日期

  • 日期格式转String
    SimpleDateFormat
    1
    2
    3
    Date date= new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String str=sdf.format(date);
  • String转日期
    1
    2
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
    Date date = sdf.parse(str);

集合框架类

Collection

Collections:提供了对集合进行排序、遍历等多种算法实现,是一个类

List 接口

List:有序的,可重复的

  1. ArrayList

    底层是数组实现的,数组没变,初始容量为10,(空间不够)创建新数组,复制原来的数组的值 ,增加新的值进去,变得是数组引用指向(指向了新的数组)

    • 特性:

      • 空间连续
      • 有序的
      • 数据可重复(只能存 引用型)
      • 对中间节点,查询快,但是做增删操作,效率低下。
    • 优点:单链表,查询效率快

  2. LinkedList

    底层是双向链表
    适合存储大量数据,

    • 优点:查询慢,但中间节点增删快,
  3. Vector

    类似于ArrayList ,但较之于它,Vector是线程安全的

Set 接口

HashSet : 底层是Hash表

其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
无序集合、不能重复
相比set接口,HashSet 多了一个clone()方法。

特点:

  • 不能保证元素的排列顺序,顺序有可能发生变化
  • 不是同步的
  • 集合元素可以是 null,但只能放入一个 null

一般操作 HashSet 还是调用 Collection 的 add / remove 等方法进行操作

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 HashSetTest {

public static void main(String[] args) {
//增加
Set<String> hashSet = new HashSet<String>();
hashSet.add("1");
hashSet.add("2");
hashSet.add("3");
hashSet.add("4");
hashSet.add("5");

//删除
hashSet.remove("1");

//查询 无法获取某个元素
System.out.println("是否包含1元素:" + hashSet.contains("2"));

//迭代
Iterator<String> it = hashSet.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
}
}

当向 HashSet 结合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在 HashSet 中存储位置。根据这种方式可以看出,HashSet 的数据存取其实是通过哈希算法实现的,因为通过哈希算法可以极大的提高数据的读取速度。通过阅读 JDK 源码,我们知道 HashSet 是通过 HashMap 实现的,只不过是HashSet 的 value 上的值都是 null 而已

简单的说,HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等,并且两个对象的hashCode() 方法返回值相等。

注意,如果要把一个对象放入 HashSet 中,重写该对象对应类的 equals() 方法,也应该重写其 hashCode() 方法。其规则是如果两个对 象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

LinkedHashSet

LinkedHashSet 在迭代访问 Set 中的全部元素时,性能比 HashSet 好,但是插入时性能稍微逊色于HashSet(因为 HashSet 直接采用哈希算法,而 LinkedHashSet 还需要维护链表结构)。

TreeSet

SortedSet 接口的唯一实现类,TreeSet 可以确保集合元素处于排序状态,这也是 TreeSet最大的特征之一。

底层是最优二叉树

Map集合

  • Map中的元素是两个对象,一个对象作为键,一个对象作为值。键不可以重复,但是值可以重复。
  • Map存储元素使用put方法,Collection使用add方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Map学习体系:
    ---| Map 接口 将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
    ---| HashMap 采用哈希表实现,所以无序
    ---| TreeMap 可以对健进行排序

    ---|Hashtable:
    底层是哈希表数据结构,线程是同步的,不可以存入null键,null值。
    效率较低,被HashMap 替代。
    ---|HashMap:
    底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。
    要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
    ---| LinkedHashMap:
    该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。
    ---|TreeMap:
    底层是二叉树数据结构。可以对map集合中的键进行排序。需要使用Comparable或者Comparator 进行比较排序。return 0,来判断键的唯一性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。

我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。

通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。

因此,正确使用Map必须保证:

作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true

作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:

如果两个对象相等,则两个对象的hashCode()必须相等;
如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
即对应两个实例a和b:

如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode();
如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。
上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。

而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。

编写hashCode和equals

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
package cn.blue;

import java.util.List;
import java.util.Objects;

/**
* @author Blue
* @date 2020/4/26
* 调用List的contains()、indexOf()这些方法,给Person类增加equals方法
**/
public class Equals {
public static void main(String[] args) {
List<Person> list = List.of(
new Person("Xiao", "Ming", 18),
new Person("Xiao", "Hong", 25),
new Person("Bob", "Smith", 20)
);
boolean exist = list.contains(new Person("Bob", "Smith", 20));
System.out.println(exist ? "测试成功!" : "测试失败!");
}

/**
* 必须要在new Person()前加载此class类,否则怎么创建person实例
*/
static class Person {
String firstName;
String lastName;
int age;

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

@Override
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
// 比较字符串使用equals,比较int类型可使用==
// return (this.lastName == p.lastName) || (this.lastName != null &&
// this.lastName.equals( p.lastName)) && this.age == p.age;

// Objects内方法
// public static boolean equals(Object a, Object b) {
// return (a == b) || (a != null && a.equals(b));
// }
return Objects.equals(this.lastName, p.lastName) &&
Objects.equals(this.firstName, p.firstName) && this.age == p.age;
}
return false;
}

@Override
public int hashCode() {
// 继承了hashCode()方法
return Objects.hash(firstName, lastName, age);
}

// 注意到String类已经正确实现了hashCode()方法,我们在计算Person的hashCode()时,
// 反复使用31*h,这样做的目的是为了尽量把不同的Person实例的hashCode()均匀分布到整个int范围。
// @Override
// public int hashCode() {
// int h = 0;
// h = 31 * h + this.firstName.hashCode();
// h = 31 * h + this.lastName.hashCode();
// h = 31 * h + this.age;
// return h;
// }
}
}
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
package cn.blue;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
* @author Blue
* 常见的map遍历操作
* @date 2020/4/26
**/
public class TraversalMap {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
// 遍历值
map.values().stream().forEach(System.out::println);
// 遍历键
map.keySet().stream().forEach(System.out::println);
// 要键值对一起遍历
map.forEach((k, v) -> {
System.out.println(k + "," + v);
});

for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}

// Map中采用Entry内部类来表示一个映射项,Map.Entry里面包含getKey()和getValue()方法
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + " " + value);
}

System.out.println("================");
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + " " + value);
}
}
}
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
package cn.blue;

import java.util.HashMap;
import java.util.Map;

/**
* @author Blue
* @date 2020/4/26
* 用Map来实现根据name查询某个Student
* 始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
**/
public class MapDemo {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 99);
Map<String, Student> map = new HashMap<>();
// 将"Xiao Ming"和Student实例映射并关联
map.put("Xiao Ming", s);
// 通过key查找并返回映射的Student实例
Student target = map.get("Xiao Ming");
// true,同一个实例 Student{name='Xiao Ming', score=99}
System.out.println(target.toString());
// 99
System.out.println(target.score);
// 通过另一个key查找
Student another = map.get("Bob");
// 未找到返回null
System.out.println(another);
}

static class Student {
public String name;
public int score;

public Student(String name, int score) {
this.name = name;
this.score = score;
}

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

Map 进阶学习

使用Iterator

倒序遍历集合

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
package cn.blue;

import java.util.*;

/**
* 倒序遍历集合
* @author Blue
*/
public class Main {
public static void main(String[] args) {
ReverseList<String> reverseList = new ReverseList<>();
reverseList.add("Apple");
reverseList.add("Orange");
reverseList.add("Pear");
for (String s : reverseList) {
System.out.println(s);
}
}
}

class ReverseList<T> implements Iterable<T> {

private List<T> list = new ArrayList<>();

public void add(T t) {
list.add(t);
}

@Override
public Iterator<T> iterator() {
return new ReverseIterator(list.size());
}

class ReverseIterator implements Iterator<T> {
int index;

ReverseIterator(int index) {
this.index = index;
}

@Override
public boolean hasNext() {
return index > 0;
}

@Override
public T next() {
index--;
// 指向了上面 private List<T> list = new ArrayList<>();
return ReverseList.this.list.get(index);
}
}
}

小结

1
2
3
4
5
Iterator是一种抽象的数据访问模型。使用Iterator模式进行迭代的好处有:

对任何集合都采用同一种访问模型;
调用者对集合内部结构一无所知;
集合类返回的Iterator对象知道如何迭代。

IO 流

File 类

遍历文件和目录

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
package cn.blue;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;

/**
* @author Blue
* @date 2020/5/1
* 遍历文件和目录
**/
public class FileDemo {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
// 列出所有文件和子目录
File[] fs1 = f.listFiles();
printFiles(fs1);
// 仅列出.exe文件
File[] fs2 = f.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// 返回true表示接受该文件
return name.endsWith(".exe");
}
});
printFiles(fs2);
}

static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}

按层次打印目录下的所有子目录和文件

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
package cn.blue;

import java.io.File;
import java.io.IOException;

/**
* Learn Java from https://www.liaoxuefeng.com/
*
* @author Blue
* 利用File对象列出指定目录下的所有子目录和文件,并按层次打印。
*/
public class FileDe2 {

public static void main(String[] args) throws IOException {
File currentDir = new File(".");
// 绝对路径
System.out.println(currentDir.getAbsolutePath());
listDir(currentDir.getCanonicalFile(), 0);
}

static void listDir(File dir, int level) {
// 递归打印所有文件和子文件夹的内容,一层层文件目录下去
File[] fs = dir.listFiles();
if (fs != null) {
for (File f : fs) {
//根据当前目录的层级打印空格
for (int i = 0; i < level; i++) {
System.out.print(" ");
}
//如果是目录,继续递归
if (f.isDirectory()) {
System.out.println(f.getName());
try {
// getCanonicalPath 会将文件路径解析为与操作系统相关的唯一的规范形式的字符串
// 与getCanonicalFile相比,它会捕获异常
listDir(f.getCanonicalFile(), level + 1);
} catch (IOException e) {
e.printStackTrace();
}
//否则直接输出文件名
} else {
System.out.println(f.getName());
}
}
}

}
}

File 类

InputStream

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
package cn.blue;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Blue
* @date 2020/5/1
**/
class InputStreamDemo {
public static void main(String[] args) {
// 创建一个FileInputStream对象:
InputStream input = null;
try {
// 项目根目录下
input = new FileInputStream("readme.txt");
while(true) {
// 反复调用read()方法,直到返回-1
int n = input.read();
if (n == -1) {
break;
}
// 打印byte的值
System.out.println(n);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流
try {
// 断言
assert input != null;
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

用try … finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource) 的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.blue;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
* @author Blue
* @date 2020/5/1
**/
class InputStreamDemo2 {
public static void main(String[] args) throws IOException {
// 编译器在此自动为我们写入finally并调用close()
try (InputStream input = new FileInputStream("readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
}
}
}

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = …)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStream和OutputStream都实现了这个接口,因此,都可以用在try(resource)中。

缓冲,一次性读取多个字节到缓冲区

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try (InputStream input = new FileInputStream("readme.txt")) {
    // 定义1000个字节大小的缓冲区:
    byte[] buffer = new byte[1000];
    int n;
    // 读取到缓冲区(在循环里面赋值不会死循环是因为n的值有更新,但不能int n = input.read(buffer);)
    while ((n = input.read(buffer)) != -1) {
    // 打印读取的文字
    String str = new String(buffer);
    System.err.println(str);
    }
    }

阻塞
在调用InputStream的read()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:

1
2
3
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;

执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

OutputStream

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
package cn.blue;

import java.io.*;
import java.nio.Buffer;
import java.nio.charset.StandardCharsets;

/**
* @author Blue
* @date 2020/5/1
**/
public class OutputStreamDemo {
public static void main(String[] args) throws IOException {
try (OutputStream output = new FileOutputStream("readme.txt")) {
// Hello World
output.write("Hello ".getBytes(StandardCharsets.UTF_8));
output.write("World".getBytes(StandardCharsets.UTF_8));
try (InputStream input = new FileInputStream("readme.txt")) {
byte[] buffer = new byte[100];
int n;
while ((n = input.read(buffer)) != -1) {
String s = new String(buffer);
System.err.println(s);
}
}
}
}
}
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
package cn.blue;

import java.io.*;

/**
* @author Blue
* @date 2020/5/1
* 利用缓冲流读取文件,以及拷贝到另一个文件
**/
public class BufferedDemo {
public static void main(String[] args) throws IOException {
BufferedReader br = null;
BufferedWriter bw = null;
InputStreamReader ir = null;
OutputStreamWriter osw = null;
String str = null;
try (InputStream fis = new FileInputStream(new File("readme.txt"))) {
OutputStream fos = new FileOutputStream(new File("out.txt"));
// 转换流(字节流 --> 字符流)
ir = new InputStreamReader(fis);
osw = new OutputStreamWriter(fos);
// 缓冲流(需要字符流)
br = new BufferedReader(ir);
bw = new BufferedWriter(osw);
while ((str = br.readLine()) != null) {
System.out.println(str);
if (bw != null) {
bw.write(str);
bw.newLine();
bw.flush();
}
}
}
}
}
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
package cn.blue;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.zip.ZipEntry;

/**
* @author Blue
* @date 2020/5/1
* 读取文件,顺便把自定义的内容写进去。
**/
public class FileReaderDemo {
public static void main(String[] args) throws IOException {
File fi = new File("src/read.txt");
if (!fi.exists()) {
// 创建read.txt文件而不是fi.mkdirs()创建文件夹
fi.createNewFile();
}
// FileReader实现了文件字符流输入,使用时需要指定编码;
try (Reader reader = new FileReader("readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
System.out.println((new String(buffer)));
}
// 打印流
PrintStream ps = new PrintStream("readme.txt");
// 这里ps需要调方法写出值,不然就readme.txt为空了
System.setOut(ps);
}
// PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("Hello");
pw.println(12345);
pw.println(true);
}
}
}

IO 流

日期与时间

Date和Calendar

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
package cn.blue;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
* @author Blue
* @date 2020/5/2
**/
public class TimeDemo {
public static void main(String[] args) {
// 获取时间戳
System.out.println(System.currentTimeMillis());
// 获取当前时间:
Date date = new Date();
// 必须加上1900
System.out.println(date.getYear() + 1900);
// 0~11,必须加上1
System.out.println(date.getMonth() + 1);
// 1~31,不能加1
System.out.println(date.getDate());
// 转换为String:
System.out.println(date.toString());
// 转换为GMT时区:
System.out.println(date.toGMTString());
// 转换为本地时区:
System.out.println(date.toLocaleString());
// 格式化时间:2020-05-02 09:44:28
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// M:输出9
//MM:输出09
//MMM:输出Sep
//MMMM:输出SeptemberSat
// May 02, 2020
var sdf1 = new SimpleDateFormat("E MMM dd, yyyy");

// Calendar 获取当前时间:
Calendar c = Calendar.getInstance();
// 年
int y = c.get(Calendar.YEAR);
// 月:记得 +1
int m = 1 + c.get(Calendar.MONTH);
// 日
int d = c.get(Calendar.DAY_OF_MONTH);
// public static final int DAY_OF_WEEK = 7; 1~7分别表示周日,周一,……,周六。
int w = c.get(Calendar.DAY_OF_WEEK);
// 小时
int hh = c.get(Calendar.HOUR_OF_DAY);
// 分
int mm = c.get(Calendar.MINUTE);
// 秒
int ss = c.get(Calendar.SECOND);
// 毫秒
int ms = c.get(Calendar.MILLISECOND);
// 2020-5-2 7 9:50:43.7
System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);

// TimeZone
Calendar ca = Calendar.getInstance();
// 清除所有:
ca.clear();
// 设置2019年:
ca.set(Calendar.YEAR, 2019);
// 设置9月:注意8表示9月:
ca.set(Calendar.MONTH, 8);
// 设置2日:
ca.set(Calendar.DATE, 2);
// 设置时间:
ca.set(Calendar.HOUR_OF_DAY, 21);
ca.set(Calendar.MINUTE, 22);
ca.set(Calendar.SECOND, 23);
// 利用Calendar.getTime()可以将一个Calendar对象转换成Date对象,然后就可以用SimpleDateFormat进行格式化了
// 2019-09-02 21:22:23
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(ca.getTime()));
}
}

LocalDateTime

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
package cn.blue;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;

/**
* @author Blue
* @date 2020/5/2
* LocalDateTime
**/
public class LocalDateTimeDemo {
public static void main(String[] args) {
// 当前日期: 2020-05-02
LocalDate d = LocalDate.now();
// 当前时间: 10:06:57.508803600
LocalTime t = LocalTime.now();
// 当前日期和时间: 2020-05-02T10:06:57.508803600
LocalDateTime dt = LocalDateTime.now();
// 都严格按照ISO 8601格式打印
System.out.println(d);
System.out.println(t);
System.out.println(dt);

// 保证获取到同一时刻的日期和时间
LocalDateTime ldt = LocalDateTime.now();
// 当前日期和时间
LocalDate d1 = ldt.toLocalDate();
// 转换到当前时间
LocalTime t1 = ldt.toLocalTime();

// 指定日期和时间:通过指定的日期和时间创建LocalDateTime可以通过of()方法
// 2019-11-30, 注意11=11月
LocalDate d2 = LocalDate.of(2019, 11, 30);
// 15:16:17
LocalTime t2 = LocalTime.of(15, 16, 17);
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);

// 自定义格式化: 2020/05/02 02:21:07
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));
// 用自定义格式解析: 2019-11-30T15:16:17
LocalDateTime dt4 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
System.out.println(dt4);

LocalDateTime localDateTime = LocalDateTime.of(2019, 10, 26, 20, 30, 59);
// 2019-10-26T20:30:59
System.out.println(localDateTime);
// 加5天减3小时:
LocalDateTime dt5 = localDateTime.plusDays(5).minusHours(3);
// 2019-10-31T17:30:59
System.out.println(dt5);
// 减1月:
LocalDateTime dt6 = dt5.minusMonths(1);
// 2019-09-30T17:30:59
System.out.println(dt6);
}
}

DateTimeFormatter

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
package cn.blue;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
* @author Blue
* @date 2020/5/2
* 和SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。
* 对ZonedDateTime或LocalDateTime进行格式化,需要使用DateTimeFormatter类;
* DateTimeFormatter可以通过格式化字符串和Locale对日期和时间进行定制输出。
**/
public class DateTimeFormatterDemo {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
// 2020-05-02T10:32 GMT+08:00
System.out.println(formatter.format(zdt));

var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
// 2020 5月 02 周六 10:32
System.out.println(zhFormatter.format(zdt));

var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
// Sat, May/02/2020 10:32
System.out.println(usFormatter.format(zdt));
}
}

数据库 | 对应Java类(旧) | 对应Java类(新)
:-: | :-: | :-: | :-:
DATETIME | java.util.Date | LocalDateTime
DATE | java.sql.Date | LocalDate
TIME | java.sql.Time | LocalTime
TIMESTAMP | java.sql.Timestamp | LocalDateTime

单元测试

编写junit测试
测试代码如下:(下面以最新版本JUnit 5为例),idea可以看着junit插件,就不需要再额外配置jar管理,或者使用maven项目,自动导入。

1
2
3
4
5
6
7
8
9
public class Factorial {
public static long fact(long n) {
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
}

测试代码

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
package cn.blue;


import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.assertEquals;


/**
* @author Blue
* @date 2020/5/2
*
**/
public class DemoTest {
/**
* 方法运行前后仅运行一次,只能标注在静态方法上
*/
@BeforeAll
static void beforeAll() {
System.out.println("init once");
}

@AfterAll
static void afterAll() {
System.out.println("ending once");
}

@BeforeEach
void before() {
System.out.println("init");
}

@AfterEach
void after() {
System.out.println("ending");
}

@Test
void add() {
System.out.println((1 + 2));
}

@Test
void testFact() {
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(3628800, Factorial.fact(10));
assertEquals(2432902008176640000L, Factorial.fact(20));
}
}

输出结果:
init once
init
ending
init
3
ending
ending once

1
2
3
4
5
6
7
8
核心测试方法testFact()加上了@Test注解,这是JUnit要求的,它会把带有@Test的方法识别为测试方法。在测试方法内部,我们用assertEquals(1, Factorial.fact(1))表示,期望Factorial.fact(1)返回1。assertEquals(expected, actual)是最常用的测试方法,它在Assertion类中定义。Assertion还定义了其他断言方法,例如:

assertTrue(): 期待结果为true
assertFalse(): 期待结果为false
assertNotNull(): 期待结果为非null
assertArrayEquals(): 期待结果为数组并与期望数组每个元素的值均相等
...
运行单元测试非常简单。选中Factorial.java文件,点击Run - Run As - JUnit Test,Eclipse会自动运行这个JUnit测试,并显示结果:

使用Fixture

不必在每个测试方法中都写上初始化代码,而是通过@BeforeEach来初始化,通过@AfterEach来清理资源

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
package cn.blue;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

Calculator calculator;

@BeforeEach
public void setUp() {
this.calculator = new Calculator();
}

@AfterEach
public void tearDown() {
this.calculator = null;
}

@Test
void testAdd() {
assertEquals(100, this.calculator.add(100));
}

@Test
void testSub() {
assertEquals(-100, this.calculator.sub(100));
}
}

异常测试

1
2
3
4
5
6
7
8
9
10
11
12
public class Factorial {
public static long fact(long n) {
if (n < 0) {
throw new IllegalArgumentException();
}
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
}
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
package cn.blue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;

import static org.junit.jupiter.api.Assertions.*;

class FactorialTest {

@Test
void testNegative() {
assertThrows(IllegalArgumentException.class, new Executable() {
@Override
public void execute() throws Throwable {
// 如果不满足条件的话,就会抛出异常,又或者异常类型不对
// org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown
Factorial.fact(-1);
}
});
}

/**
* 链式写法
*/
@Test
void testNegativeSimple() {
assertThrows(IllegalArgumentException.class, () -> {
Factorial.fact(-1);
});
}
}

条件测试
在运行测试的时候,有些时候,我们需要排出某些@Test方法,不要让它运行,这时,我们就可以给它标记一个 @Disabled

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
package cn.blue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import static org.junit.jupiter.api.Assertions.*;

class ConfigTest {
Config config;

/**
* @EnableOnOs就是一个条件测试判断,在windows下执行
*/
@Test
@EnabledOnOs(OS.WINDOWS)
void testWindows() {
assertEquals("C:\\test.ini", config.getConfigFile("test.ini"));
}

@Test
@EnabledOnOs({ OS.LINUX, OS.MAC })
void testLinuxAndMac() {
assertEquals("/usr/local/test.cfg", config.getConfigFile("test.cfg"));
}
}

参数化测试

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
package cn.blue;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class StringUtilsTest {

/**
* @MethodSource注解,它允许我们编写一个同名的静态方法来提供测试参数:
*/
@ParameterizedTest
@MethodSource
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}

static List<Arguments> testCapitalize() {
return List.of(
Arguments.arguments("abc", "Abc"),
Arguments.arguments("APPLE", "Apple"),
Arguments.arguments("gooD", "Good"));
}
}

正则表达式

分组匹配

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
package cn.blue.pattern;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author Blue
* @date 2020/5/2
**/
public class Exercise {

public static void main(String[] args) {
String regex = "([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher("08:45:34");
if (matcher.matches()) {
// 匹配成功,Matcher.group(index)返回子符串:
// 1表示第一个子串,2表示第二个子串,3表示第三个子串。
// 如果我们传入0会得到什么呢?答案是08:45:34,即整个正则匹配到的字符串。
// 08
String s1 = matcher.group(1);
// 45
String s2 = matcher.group(2);
// 34
String s3 = matcher.group(3);
} else {
System.out.println("失败");
}
}
}

非贪婪匹配

1
2
3
这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+总是会把后面的0包含进来。

要让\d+尽量少匹配,让0*尽量多匹配,我们就必须让\d+使用非贪婪匹配。在规则\d+后面加个?即可表示非贪婪匹配。我们改写正则表达式如下:
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d+?)(0*)");
Matcher matcher = pattern.matcher("1230000");
if (matcher.matches()) {
System.out.println("group1=" + matcher.group(1)); // "123"
System.out.println("group2=" + matcher.group(2)); // "0000"
}
}
}
1
2
3
因此,给定一个匹配规则,加上?后就变成了非贪婪匹配。

我们再来看这个正则表达式(\d??)(9*),注意\d?表示匹配0个或1个数字,后面第二个?表示非贪婪匹配,因此,给定字符串"9999",匹配到的两个子串分别是""和"9999",因为对于\d?来说,可以匹配1个9,也可以匹配0个9,但是因为后面的?表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个9。

搜索和替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
模板引擎是指,定义一个字符串作为模板:

Hello, ${name}! You are learning ${lang}!
其中,以${key}表示的是变量,也就是将要被替换的内容

当传入一个Map<String, String>给模板后,需要把对应的key替换为Map的value。

例如,传入Map为:

{
"name": "Bob",
"lang": "Java"
}
然后,${name}被替换为Map对应的值"Bob”,${lang}被替换为Map对应的值"Java",最终输出的结果为:

Hello, Bob! You are learning Java!
请编写一个简单的模板引擎,利用正则表达式实现这个功能。

我们获取到Matcher对象后,不需要调用matches()方法(因为匹配整个串肯定返回false),而是反复调用find()方法,在整个串中搜索能匹配上规则的子串,并打印出来。

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
package cn.blue.pattern;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author Blue
* @date 2020/5/2
**/
public class TemplateDemo {
public static void main(String[] args) {
String s = "Hello, ${name}! You are learning ${lang}!";
Template template = new Template(s);
Map<String, String> map = new HashMap<>();
map.put("name", "Bob");
map.put("lang", "Java");
System.out.println(template.render(map));
}
}

class Template {
private final String template;
private final Pattern pattern = Pattern.compile("\\$\\{(\\w+)}");

public Template(String template) {
this.template = template;
}

public String render(Map<String, String> data) {
Matcher m = pattern.matcher(template);
StringBuffer sb = new StringBuffer();
while (m.find()) {
// Start 方法返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引,
// end 方法最后一个匹配字符的索引加 1。
String key = template.substring(m.start() + 2, m.end() - 1);
m.appendReplacement(sb, data.get(key));
}
m.appendTail(sb);
return sb.toString();
}
}

输出:Hello, Bob! You are learning Java!

多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

多线程模型是Java程序最基本的并发模型;
后续读写网络、数据库、Web开发等都依赖Java多线程模型。

创建新线程
方法一:从Thread派生一个自定义类,然后覆写run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}

执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。

方法二:创建Thread实例时,传入一个Runnable实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}

特殊方式:Java8引入的lambda语法进一步简写为:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}

使用线程执行的打印语句,和直接在main()方法执行有区别吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
我们用蓝色表示主线程,也就是main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。

接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread run和thread end语句。

当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。

我们再来看线程的执行顺序:

1. main线程肯定是先打印main start,再打印main end;
2. t线程肯定是先打印thread run,再打印thread end。
但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。

要模拟并发执行的效果,我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
try {
Thread.sleep(10);
} catch (InterruptedException e) {}
System.out.println("thread end.");
}
};
t.start();
// 让main方法休眠20毫秒,再打印main end...
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
System.out.println("main end...");
}
}

要特别注意:直接调用Thread实例的run()方法是无效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.run();
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello");
}
}

直接调用 run() 方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。

必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0() 方法,native 修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

1
2
3
4
5
6
7
8
9
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;

一个线程对象只能调用一次start()方法;

线程的执行代码写在run()方法中;

线程调度由操作系统决定,程序本身无法决定调度顺序;

Thread.sleep()可以把当前线程暂停一段时间。

线程的状态

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join() 等待t线程结束后再继续运行:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}

输出

start
hello
end

当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

1
2
3
4
5
6
7
8
9
Java线程对象Thread的状态包括:

New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;

通过对另一个线程对象调用join()方法可以等待其执行结束;

可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

对已经运行结束的线程调用join()方法会立刻返回。

yield()方法与join()方法
yield()方法是这样描述的:暂停当前正在执行的线程对象,并执行其他线程。在多线程的情况下,由CPU决定执行哪一个线程,而yield()方法就是暂停当前的线程,让给其他线程(包括它自己)执行,具体由谁执行由CPU决定。通俗来说是可以达到让步目的,但是最终谁被再次调用执行还是要看cpu调度的。注意,yield是静态方法

join()方法是指等待调用join()方法的线程执行结束,程序才会继续执行下去,这个方法适用于:一个执行程序必须等待另一个线程的执行结果才能够继续运行的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
JoinRunnable runnable1 = new JoinRunnable();
Thread thread1 = new Thread(runnable1, "线程1");

System.out.println("主线程开始执行!");
thread1.start();

try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("主线程执行结束!");
}
1
2
3
4
5
6
7
8
9
执行结果:
主线程开始执行!
线程1开始执行!
线程1执行了[1]次
线程1执行了[2]次
线程1执行了[3]次
线程1执行了[4]次
线程1执行了[5]次
主线程执行结束!

从执行结果可以看出,加入join()方法,主线程启动了子线程之后,在等待子线程执行完毕才继续执行下面的操作。

网络编程

网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect)网络模型是ISO组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

常用协议

IP协议是一个分组交换,它不保证可靠传输。而TCP协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP协议是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。

TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。

UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。

选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。

TCP编程

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
package cn.blue.socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
* @author Blue
* @date 2020/5/3
**/
public class Server {
public static void main(String[] args) throws IOException {
// 监听指定端口
ServerSocket ss = new ServerSocket(6666);
System.out.println("server is running...");
for (; ; ) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}

class Handler extends Thread {
Socket sock;

public Handler(Socket sock) {
this.sock = sock;
}

@Override
public void run() {
try (InputStream input = this.sock.getInputStream();
OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}

private void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
writer.flush();
for (; ; ) {
String s = reader.readLine();
// 读取到bye,服务器就关闭
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
// 调用flush()强制把缓冲区数据发送出去
writer.flush();
}
}
}
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
package cn.blue.socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
* @author Blue
* @date 2020/5/3
**/
public class Client {
public static void main(String[] args) throws IOException {
// 连接指定服务器和端口
Socket sock = new Socket("localhost", 6666);
try (InputStream input = sock.getInputStream()) {
try (OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
}
sock.close();
System.out.println("disconnected.");
}

private static void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (;;) {
// 打印提示
System.out.print(">>> ");
// 读取一行输入
String s = scanner.nextLine();
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if ("bye".equals(resp)) {
break;
}
}
}
}

多发互聊

服务器

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
package cn.blue.chart;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

public class Test {
public static void main(String[] args) {
ServerSocket ss = null;
Socket s = null;
BufferedReader br = null;
PrintWriter out = null;
BufferedReader brInput = null;
try {
ss = new ServerSocket(9001);
System.out.println("服务器正在监听9001端口");
s = ss.accept();
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
out = new PrintWriter(s.getOutputStream(), true);
brInput = new BufferedReader(new InputStreamReader(System.in));
String ip = s.getInetAddress().getHostAddress();
out.println("你好啊");
while (true) {
String msgFromClient = br.readLine();
System.out.println(ip + "说:" + msgFromClient);
System.out.print("您说:");
String msgToClient = brInput.readLine();
out.println(msgToClient);
}

} catch (SocketException e) {
System.out.println("对方已经下线或网络断开");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (brInput != null) {
try {
brInput.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (out != null) {
out.close();
}
if (s != null) {
try {
s.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

客户端

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
package cn.blue.chart;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
Socket s = null;
BufferedReader br = null;
PrintWriter out = null;
BufferedReader brInput = null;
try {
s = new Socket(InetAddress.getByName("127.0.0.1"),9001);
// 接收对面输过来的
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
out = new PrintWriter(s.getOutputStream(),true);
// 自己输进去的
brInput = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String msgFromServer = br.readLine();
System.out.println("服务器说:"+msgFromServer);
System.out.print("您说:");
String msgToServer = brInput.readLine();
out.println(msgToServer);
}

} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
if(brInput != null) {
try {
brInput.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(br != null) {
try {
br.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(out != null) {
out.close();
}
if(s != null) {
try {
s.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}

发送邮件

1.引入相关依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>

2.通过JavaMail API连接到SMTP服务器上:

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
 // 服务器地址:
String smtp = "smtp.qq.com";
// 登录用户名:
String username = "youremail@qq.com";
// 登录口令:
String password = "lhmaqecoacwcbjci";
// 连接到SMTP服务器465端口:
Properties props = new Properties();
// SMTP主机名
props.put("mail.smtp.host", smtp);
// 主机端口号
props.put("mail.smtp.port", "465");
// 是否需要用户认证
props.put("mail.smtp.auth", "true");
// 启用调试
props.setProperty("mail.debug", "true");
// 启用TLS加密
props.put("mail.smtp.starttls.enable", "true");
// 设置链接超时
props.put("mail.smtp.timeout ", "10000");
// 设置ssl端口
props.setProperty("mail.smtp.socketFactory.port", "465");
props.setProperty("mail.smtp.socketFactory.fallback", "false");
// 使用ssl协议来保证连接安全
props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
// 获取Session实例:
Session session = Session.getInstance(props, new Authenticator() {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
// 设置debug模式便于调试:
session.setDebug(true);

3.发送邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
MimeMessage message = new MimeMessage(session);
// 设置发送方地址:
message.setFrom(new InternetAddress("youremail@qq.com"));
// 设置接收方地址:
message.setRecipient(Message.RecipientType.TO, new InternetAddress("toemail@qq.com"));
// 设置邮件主题:
message.setSubject("发送内嵌图片的HTML邮件", "UTF-8");
// 设置邮件文本正文:
// message.setText("你好呀 小朋友❤...", "UTF-8");
// 发送HTML邮件: <img src="cid:img01" />
String body = "<h1>Hello 小朋友❤...</h1> <img src=\"cid:img01\" /><p>Hi <a href='https://coderblue.cn'>这是我的博客</a></p>";
// 不带附件、图片的html文本
message.setText(body, "UTF-8", "html");

附件和内嵌图片公用一个 multipart

1
2
3
4
5
Multipart multipart = new MimeMultipart();
// 添加text:
BodyPart textpart = new MimeBodyPart();
textpart.setContent(body, "text/html;charset=utf-8");
multipart.addBodyPart(textpart);

4.携带附件发送

1
2
3
4
5
6
7
8
9
// 发送附件
// 添加image:
BodyPart imagepart = new MimeBodyPart();
imagepart.setFileName("图片.jpg");
// 读取文件
InputStream input = new FileInputStream("D:\\Blog\\public\\images\\ayer.png");
// 二进制文件可以用application/octet-stream,Word文档则是application/msword。
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "application/octet-stream")));
multipart.addBodyPart(imagepart);

5.HTML内嵌图片

1
2
3
4
5
6
7
8
9
// 发送内嵌图片的HTML邮件
BodyPart imagepart1 = new MimeBodyPart();
InputStream input1 = new FileInputStream("D:\\Blog\\public\\images\\blue.jpg");
// 根据图片类型使用image/jpeg或image/png
imagepart1.setDataHandler(new DataHandler(new ByteArrayDataSource(input1, "image/jpeg")));
// 与HTML的<img src="cid:img01">关联:
imagepart1.setHeader("Content-ID", "img01");
imagepart1.setFileName("blue.jpg");
multipart.addBodyPart(imagepart1);

6.将附件和图片都添加进去

1
2
// 设置邮件内容为multipart:
message.setContent(multipart);

7.发送邮件

1
2
// 发送:
Transport.send(message);

HTTP编程

HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:

  • Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别用于请求;
  • User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型;
  • Accept:表示客户端能处理的HTTP响应格式,/表示任意格式,text/*表示任意文本,image/png表示PNG格式的图片;
  • Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。

如果是GET请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是POST请求,那么该HTTP请求带有Body,以一个空行分隔。一个典型的带Body的HTTP请求如下:

1
2
3
4
5
6
POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

username=hello&password=123456

TCP的三次握手与四次挥手理解及面试题
字段 | 含义
|–|–|
URG | 紧急指针是否有效。为1,表示某一位需要被优先处理
ACK | 确认号是否有效,一般置为1。
PSH | 提示接收端应用程序立即从TCP缓冲区把数据读走。
RST | 对方要求重新建立连接,复位。
SYN | 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
FIN | 希望断开连接。

三次握手过程理解

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

TCP的三次握手与四次挥手理解及面试题

使用Jackson解析dom

定义好JavaBean

1
2
3
4
5
6
7
8
public class Book {
public long id;
public String name;
public String author;
public String isbn;
public List<String> tags;
public String pubDate;
}

一个名叫Jackson的开源的第三方库可以轻松做到XML到JavaBean的转换

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<version>4.4.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 读取resources目录下的资源
InputStream input = Main.class.getResourceAsStream("/book.xml");
JacksonXmlModule module = new JacksonXmlModule();
XmlMapper mapper = new XmlMapper(module);
Book book = null;
try {
// 直接读取XML并返回一个JavaBean
book = mapper.readValue(input, Book.class);
System.out.println(book.id);
System.out.println(book.name);
System.out.println(book.author);
System.out.println(book.isbn);
System.out.println(book.tags);
System.out.println(book.pubDate);
} catch (IOException e) {
e.printStackTrace();
}

输出

1
2
3
4
5
6
1
Java核心技术
Cay S. Horstmann
1234567
[Java, Network]
null

JDBC编程

JDBC 查询

注意到这里添加依赖的scope是runtime,因为编译Java程序并不需要MySQL的这个jar包,只有在运行期才需要使用。如果把runtime改成compile,虽然也能正常编译,但是在IDE里写程序的时候,会多出来一大堆类似com.mysql.jdbc.Connection这样的类,非常容易与Java标准库的JDBC接口混淆,所以坚决不要设置为compile。

这里使用,mysql8.0

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/easypoi?serverTimezone=GMT%2b8";
String JDBC_USER = "root";
String JDBC_PASSWORD = "密码";
// Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("select * from userinfo")) {
// ResultSet获取列时,索引从1开始而不是0;
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// int id = rs.getInt("id"); 注意:索引从1开始
int id = rs.getInt(1);
String name = rs.getString(2);
String sex = rs.getString(3);
Date birthday = rs.getDate(4);
System.out.println(id + " " + name + " " + sex + " " + birthday);
}
}
}
}

SQL 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用Statement拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。

我们来看一个例子:假设用户登录的验证方法如下:

User login(String name, String pass) {
...
stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
...
}
其中,参数name和pass通常都是Web页面输入后由程序接收到的。

如果用户的输入是程序期待的值,就可以拼出正确的SQL。例如:name = "bob",pass = "1234":

SELECT * FROM user WHERE login='bob' AND pass='1234'
但是,如果用户的输入是一个精心构造的字符串,就可以拼出意想不到的SQL,这个SQL也是正确的,但它查询的条件不是程序设计的意图。例如:name = "bob' OR pass=", pass = " OR pass='":

SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
这个SQL语句执行的时候,根本不用判断口令是否正确,这样一来,登录就形同虚设。

要避免SQL注入攻击,一个办法是针对所有字符串参数进行转义,还有一个办法就是使用PreparedStatement。
PreparedStatement ps = conn.prepareStatement(sql);

JDBC更新

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
 // JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/easypoi?serverTimezone=GMT%2b8";
String JDBC_USER = "root";
String JDBC_PASSWORD = "密码";
// 获取连接:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
// 新增:表示JDBC驱动必须返回插入的自增主键
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO userinfo (name, sex, birthday) VALUES (?,?,?)",
Statement.RETURN_GENERATED_KEYS)) {
// id自增
ps.setObject(1, "Bob");
ps.setObject(2, "1");
ps.setObject(3, "2020-05-03");
// 受影响行数
int n = ps.executeUpdate();
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
// 获取主键
long id = rs.getLong(1);
}
}
}

// 修改
try (PreparedStatement ps = conn.prepareStatement("UPDATE userinfo SET name=? WHERE id=?")) {
ps.setObject(1, "Jim");
ps.setObject(2, 3);
// 返回更新的行数
int n = ps.executeUpdate();
}

// 删除
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM userinfo WHERE id=?")) {
ps.setObject(1, 4);
int n = ps.executeUpdate();
}

// 查询
try (PreparedStatement ps = conn.prepareStatement("select * from userinfo")) {
// ResultSet获取列时,索引从1开始而不是0;
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// int id = rs.getInt("id"); 注意:索引从1开始
int id = rs.getInt(1);
String name = rs.getString(2);
String sex = rs.getString(3);
Date birthday = rs.getDate(4);
System.out.println(id + " " + name + " " + sex + " " + birthday);
}
}
}
}

JDBC事务

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read)
| —— | —— | —— | —— |
Read Uncommitted | Yes | Yes | Yes
Read Committed | - | Yes | Yes
Repeatable Read | - | - | Yes
Serializable | - | - | -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Connection conn = openConnection();
try {
// 关闭默认的自动提交:
conn.setAutoCommit(false);
// 执行多条SQL语句:
insert(); update(); delete();
// 提交事务:
conn.commit();
} catch (SQLException e) {
// 回滚事务:
conn.rollback();
} finally {
conn.setAutoCommit(true);
conn.close();
}

Web开发

Web基础

编写HTTP Server

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package cn.blue.web;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
* @author Blue
* @date 2020/5/3
**/
public class Server {
public static void main(String[] args) throws IOException {
// 监听指定端口
ServerSocket ss = new ServerSocket(8080);
System.out.println("server is running...");
for (; ; ) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}

class Handler extends Thread {
Socket sock;

public Handler(Socket sock) {
this.sock = sock;
}

@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}

private void handle(InputStream input, OutputStream output) throws IOException {
System.out.println("Process new http request...");
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
// 读取HTTP请求:
int requestFlag = -1;
String first = reader.readLine();
if (first.startsWith("GET / HTTP/1.")) {
requestFlag = 1;
}
if (first.startsWith("GET /favicon.ico HTTP/1.")) {
requestFlag = 2;
}
for (; ; ) {
String header = reader.readLine();
// 读取到空行时, HTTP Header读取完毕
if (header.isEmpty()) {
break;
}
System.out.println(header);
}
System.out.println(requestFlag > 0 ? "Response OK" : "Response Error");
switch (requestFlag) {
case -1:
writer.write("404 Not Found\r\n");
writer.write("Content-Length: 0\r\n");
writer.write("\r\n");
writer.flush();
break;
case 1:
// 发送成功响应:
String data = "<html><body><h1>Hello, world!</h1></body></html>";
int length = data.getBytes(StandardCharsets.UTF_8).length;
writer.write("HTTP/1.0 200 OK\r\n");
writer.write("Connection: close\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("Content-Length: " + length + "\r\n");
// 空行标识Header和Body的分隔
writer.write("\r\n");
writer.write(data);
writer.flush();
break;
case 2:
// 把图片放在bin下
byte[] b = Server.class.getResourceAsStream("/favicon.png").readAllBytes();
writer.write("HTTP/1.0 200 OK\r\n");
writer.write("Connection: close\r\n");
writer.write("Content-Type: image/x-icon\r\n");
writer.write("Content-Length: " + b.length + "\r\n");
// 空行标识Header和Body的分隔
writer.write("\r\n");
writer.flush();
output.write(b);
output.flush();
break;
default:
}
}
}

Spring开发

Ioc原理


IoC又称为依赖注入,它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各组件的依赖关系。一种最简单的配置是通过XML文件来实现,例如:

bean标签中的class一定要写成全类名的形式,因为spring是根据类的全类名进行反射获取类的。

1
2
3
4
5
6
7
8
9
<beans>
<bean id="dataSource" class="cn.blue.HikariDataSource" />
<bean id="bookService" class="BookService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="cn.blue.UserService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>

上述XML配置文件指示IoC容器创建3个JavaBean组件,并把id为dataSource的组件通过属性dataSource(即调用setDataSource()方法)注入到另外两个组件中。

在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。

构造方法注入

1
2
3
4
5
6
7
public class BookService {
private DataSource dataSource;

public BookService(DataSource dataSource) {
this.dataSource = dataSource;
}
}

属性注入
属性注入是通过Car类的set方法进行注入的,因此一定要对类的属性定义set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Car {

private String brand;
private float price;

public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
@Override
public String toString() {
return "Car [brand=" + brand + ", price=" + price + "]";
}

}

Spring的IoC容器同时支持属性注入和构造方法注入,并允许混合使用。

注入对象类型属性service-dao

Spring的生命周期
加载bean,实例化bean,调用生命周期的第一个方法:init-method
再就是DI–>注入操作–>再destory-method

bean的作用域–scope


SpringMVC

常用配置

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
1.导包 spring-webmvc
2.创建配置文件 /WEB-INF/{servletName}-servlet.xml:文件名-->dispatcher-servlet.xml
3.配置SpringMvc:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
">
1.开启扫描 :
<context:component-scan base-package="cn.jasonone.controller"></context:component-scan>
2.配置视图解析器:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
注: 视图访问 prefix+viewName+suffix
4.修改web.xml:
<servlet>
<servlet-name>ds</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ds</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

存入Session域

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020 Liu Yang    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信