首先申明,是同班同学wbb的,我拿来应付面试的。

JAVA语言

1 Java语言基础

1.2 Java简介

1.2.1 什么是程序

例:从学校宿舍到艾瑞来上课的过程

程序一词来自生活,通常指完成某些事情的一种既定方式和过程

可以将程序看成对一系列动作的执行过

程的描述。

最后引入计算机程序的概念:

计算机程序:

为了让计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合

1.2.1.1 java起源

简单介绍java起源,James Gosling,嵌入式机顶盒,Java岛(爪哇岛),以此命名

image-20201013085621251

1.2.1.2 java发展史

从jkd 1.0 到最新的JDK 14, java的技术平台简介,java ME、java SE、java EE

1.2.1.3 jvm虚拟机

简单介绍java发展史中几个重要的jvm虚拟机版本。jvm虚拟机的重要作用。

1.2.1.4 java环境搭建

演示在windows10 系统的环境搭建与在centos7 系统的java开发环境搭建。

编写第一个java程序HelloWorld.

public class HelloWorld{
    public static void main(String[] args){
        System.out.println("Hello World");
    }
}

1.2.1.4.1 开发Java程序的三个基本步骤

1.编写源程序

2.编译源程序

​ cmd窗口中使用javac进行编译,带文件类型后缀, 如 javac helloworld.java

3.运行

​ cmd窗口中使用java命令来执行,不带文件类型后缀, 如 java helloworld

1.2.1.5java基本编码规范

  • 类名用public修饰,类名命名规则:帕斯卡命名规则(首字母大写,后续小写)
  • 一行只写一条语句
  • 花括弧{}的使用位置
  • 代码的缩进

编写一个java程序MyInfo,要求学生打印显示出自己的个人基本信息,显示内容如下:

姓名:张三
性别:男
生日:2000-10-10

print和println的区别
    print不换行打印
    println换行打印

1.3 变量&数据类型

1.3.1变量

变量命名规则

​ 驼峰规则:首字母由 小写字母 或 下划线_ 或 $符号 ,不能使用系统关键字

首字母其他字母
小写字母 或下划线_ 或$数字 或 字母 或 下划线_或 $符号

示例:name、username、_name、$name、myName

1.3.1.1 变量定义和使用步骤

第一步:声明变量,即“根据数据类型在内存申请空间”

// 数据类型 变量名
int money;    // 到银行开户

第二步:赋值,即“将数据存储至对应的内存空间”

money = 100;  // 开户后存100元到账户中
// 第一步和第二步合并
int money = 100; // 开户的同时,存100元到账户里

第三步:使用变量,即“取出数据使用”

System.out.println(money);

1.3.1.2 基本数据类型

序号数据类型说明
1byte8位整数 -128(-2^7)至 127(2^7-1) 1个字节 byte money = 100;
2short16 位整数 -32768(-2^15)至 32767(2^15 - 1) 2个字节 short monye = 100;
3int32位整数 -2^31 至 2^31 - 1 4个字节 int money = 100;
4long64位整数 -2^63 至 2^63 -1 8个字节 long money = 100L;
5float单精度32位的浮点数(小数) 4个字节 float money = 3.14f;
6double双精度64 位的浮点数 8个字节 double money = 3.14;
7boolean保存布尔值,表示真/假 取值为true和false boolean hasMoney = false;
8char保存一个16 位 Unicode 字符 从u0000 至 uffff 0-65535 char word = 'A';

1.3.1.3 基本数据类型之间的转换

  • 自动类型转换:

​ 类型要兼容、小数据类型可以自动转为大 的数据类型

int numA = 10;
double numB = numA;
  • 强制类型转换

​ 类型要兼容,大数据类型转化为小数据类型, 转换后精度会丢失:

double numA = 10.5;
int numB = (int)numA;
  • int 与 char 之间的转换(需要强制转换)

    char word = 'A';
    int num = word;
    
    int num = 65;
    char word = (char)num;

1.3.1.4 String

数据类型说明
String字符串类型,保存一串字符,String name = 'tony';

1.3.1.5 java内存的划分

堆内存和栈内存

普通数据类型:采用栈内存,将值存入栈中,先进后出

对象类型:new一个对象后,在堆出分出一块内存给此对象,将此对象的内存地址存入栈内存中,堆内存为无序存放

1.3.1.6 java程序错误的分类

//1.编译错误
// 代码语法上有错误,编译器无法通过编译
char word = "tony";

//2.运行错误
// 运行错误,代码可以正常被编译,但运行时会报错;
int num = 10/0;

//3.逻辑错误
// 逻辑错误, 代码编译正常,运行也有结果,但是结果显示不正确
int score = 59;
if(score <= 60){
    System.out.println("成绩及格")
}else{
    System.out.println("成绩不及格");
}

1.4 运算符

1.4.1 算数运算符

加 +

减 -

乘 *

除 /

自增 ++

自减 --

//++ -- 代码的含义, 并注意符号的书写位置
int numA = 1;
int numB = numA++;   //1
int numC = numA--;   //2
int numD = ++numA;   //2
int numE = --numA;   //1

image-20210424142036062

1.4.2 关系运算符

==、!=、>、>=、<、<=

关系运算符,结果保存在布尔类型的变量中, 并打印显示结果。

int numA = 10;
int numB = 20;
boolean result = numA > numB; //false

1.4.3 逻辑运算符

& | ! , 短路&&,短路||

// 逻辑与:先计算2<1的结果,再计算2<3结果,最后用俩结果进行逻辑与的运算,再把结果保存到变量中
boolean result  = 2<1 & 2<3;
// 短路与:先计算2<1的结果,如果结果为假,则不会再计算后面表达式的结果,整个结果为假。实际开发中推荐用 短路与
boolean result  = 2<1 && 2<3;
// 逻辑或:先计算2<1的结果,再计算2<3结果,最后用俩结果进行逻辑或的运算,再把结果保存到变量中
boolean result  = 2<1 | 2<3;
// 短路或:先计算2<1的结果,如果结果为真,则不会再计算后面表达式的结果,整个结果为真。实际开发中推荐用 短路或
boolean result  = 2<1 || 2<3;
// 逻辑非,取反操作,真值变为假,假值变为真
boolean result = !(2>1);

1.4.4 位运算符(二进制运算)

& | ^ ~ >> << (二进制运算)

右移运算符:>>

左移运算符:<<

1.4.5 赋值运算符

=及=与算数运算符(+-*/%的结合使用)

// 2-3行代码的含义,以及numA的值变化后,是否会对numB的值带来影响
int numA = 10;
int numB = numA;
numA = 20;

//连+等运算
int numA = 10;
// 以下两行代码功能等同
numA = numA+ 10;
numA += 10;  // 注意连+运算符的书写位置

1.4.6 运算符优先级

image-20201013111229941

1.4.7 三目运算符

简化if else操作

//三目运算符
        int flag = 5 > 3 ? 1 : 2;
        //1

判断 5>3结果
    true 则将1赋值给flag
    false则将2赋值给flag

1.5 流程控制语句

1.5.1 顺序结构

学会断点调试

// 使用代码单步运行调试调试的方式,演示numA的值改变后,对numB没有影响
int numA = 10;
int numB = numA;
numA = 20;
System.out.println(numA);
System.out.println(numB);

1.5.2 选择结构

1.5.2.1 if选择结构

打印学生成绩等级表为例

分数对应等级
60分以下E
60-70之间C
70-90之间B
90分以上A
double score = 65;

//简单if结构  只有一个分支
if (score >= 60 ){
System.out.println("及格万岁");
        }

//简单if、else结构,具有两个分支
if (score >= 60 ) {
            System.out.println("及格万岁");
        }
else System.out.println("挂科悲剧");*/

//多重if结构
if (score < 60 ) {
            System.out.println("D");
        }
        else if (score < 70 ){
            System.out.println("C");
        }
        else if (score < 90 ){
            System.out.println("B");
        }
        else System.out.println("A");

1.5.2.2switch结构

  1. | 日期 | 菜谱 |
    | ------ | ---------- |

| 星期一 | 红烧茄子 |
| 星期二 | 土豆牛腩 |
| 星期三 | 清蒸排骨 |
| 星期四 | 夫妻肺片 |
| 星期五 | 青椒土豆丝 |
| 星期六 | 大闸蟹 |
| 星期天 | 满汉全席 |

//switch选择结构,做精确匹配
        /*String date = new String("星期一");
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入当前日期");
        date = scanner.nextLine();*/
        /*switch (date){
            case "星期一":
                System.out.println("红烧茄子");
                break;
            case "星期二":
                System.out.println("土豆牛腩");
                break;
            case "星期三":
                System.out.println("请蒸排骨");
                break;
            case "星期四":
                System.out.println("夫妻肺片");
                break;
            case "星期五":
                System.out.println("青椒土豆丝");
                break;
            case "星期六":
                System.out.println("大闸蟹");
                break;
            case "星期日":
                System.out.println("满汉全席");
                break;
            default:
                System.out.println("输入有误");
                break;
        }*/



        //switch多个分支执行结果相同时的做法
        /*switch (date){
            case "星期一":
            case "星期二":
            case "星期三":
            case "星期四":
            case "星期五":
                System.out.println("青椒土豆丝");
                break;
            case "星期六":
                System.out.println("大闸蟹");
                break;
            case "星期日":
                System.out.println("满汉全席");
                break;
            default:
                System.out.println("输入有误");
                break;
        }*/

1.5.3 循环结构

//循环结构
/*设计循环的套路
有没有重复的操作
1.循环条件的初始值
2.循环条件的变更操作
3.循环体的操作
*/

1.5.3.1 do.......while

//目的:打印重复100遍
int i = 1;
        do {
            System.out.println("好好学习,天天向上!");
            i++;
        }while (i <= 100);

1.5.3.2 for循坏

适合循环次数固定的循环

// 创建扫描器对象
/*Scanner scanner = new Scanner(System.in);
// 记录罚抄的遍数
int counter = 0;
// 特点,适合于循环次数未知的情况
String answer = ""; // 我错了
for(counter = 0; ;counter ++){
    System.out.println("好好学习,天天向上");
    System.out.print("你知错了吗?");
    answer = scanner.nextLine();
    if(answer.equals("我错了"))
        break;
}

System.out.println("本次小明一共罚抄" + counter + "次。");*/

1.5.3.3break和continue

使用场合作用
break用于switch结构和循环结构中终止整个循环,程序跳转到循环块外的下一条语句
continue用于循环结构中跳出本次循环,进入下一次循环

break:用于do-while、while、for中时,break作用:可跳出循环而执行循环后面的语句

//*张三和李四打赌跑5公里,谁输了要请客撸串。*
//
//*已知操场的跑道约为400米,跑完全程需要8圈左右。*
//
//*结果在张三跑完第3圈的时候,张三感觉不行了,人都要飞起了。*
//
//*于是他终止了比赛,结果张三输了

for (int i = 0; i < 10 ; i++) {
    System.out.println("跑完第"+(i+1)+"圈");
    //张三跑完第三圈不行了,要退出比赛
    if (i == 2){
        //退出比赛
        break;
    }
}
System.out.println("比赛结束!");

continue :只能用在循环里
continue 作用:跳过循环体中剩余

//求一个简单的数学问题:1~10之间的整数相加,得到累加值大于20的当前数
int sum = 0;
for (int i = 1; i <= 10 ; i++) {
    if(sum >= 20 ){
        continue;
    }
    sum += i;
}
System.out.println(sum);
  //统计10名同学中成绩大于80的人数。
    Scanner scanner = new Scanner(System.in);
    int score = 0;
    int num = 0;
    for (int i = 0; i < 10; i++) {
        System.out.print("请输入第" + (i + 1) + "位学生的成绩: ");
        score = scanner.nextInt();
        if (score < 80) {
            continue;
        }
        num++;
    }
    System.out.println("成绩大于80的人数为:");
}

1.6数组

1.6.1 数组简介

数组也是一个变量,存储相同数据类型的一组数据

数组基本要素

  • 元素类型:数组元素的数据类型
  • 标识符:数组的名称,用于区分不同的数组
  • 数组元素:向数组中存放的数据
  • 元素下标:对数组元素进行编号,从0开始,数组中的每个元素都可以通过下标来访问

1.6.2 数组使用

(1)声明数组

数据中要保存的数据类型是什么

// 数据类型 数组名[];
int numbers[];

// 数据类型[] 数组名;
String[] names;       //推荐写法

(2)分配空间

数组要分配几个连续的空间

numbers = new int[30];
nanems  = new String[10];

// 1、2步骤可以合二为一 数据类型[ ]  数组名   =   new  数据类型[大小]  ;           
int[] numbers = new int[30];

image-20201015115313166(3)赋值

动态初始化:数组先声明、分配空间后再进行赋值

numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 10;
numbers[3] = 9;
numbers[4] = 20;
.........
numbers[29] = 50;

静态初始化: 数据声明的同时,进行初始化

int[] numbers = new int[]{1,2,3,4,5};
int[] numbers = {1,2,3,4,5};
// 错误写法
int[] numbers = new int[5]{1,2,3,4,5};

(4)处理数据

1.6.3 数组的内存划分

image-20210515110741589

1.6.4 多维数组

二重循环

eg:实现一个空的围棋的棋盘,棋盘上没有棋子,用数字0代替;

image-20201015153145386

//棋盘
        int[][] nums = new int[19][19];
        nums[2][2] = 1;
        nums[3][3] = 2;
        for (int i = 0; i <19 ; i++) {
            for (int j = 0; j <19 ; j++) {
                System.out.print(nums[i][j]);
            }
            System.out.println();
        }

eg:打印直角三角形

//打印直角三角形
        int[][] nums = new int[10][10];
        for (int i = 0; i <10 ; i++) {
            for (int j = 0; j <i ; j++) {
                System.out.print("*");
            }
            System.out.println();
        }

1.6.5 排序算法

1.6.5.1 插入算法

//实现思想
//复制旧数组的值到新数组中
//通过比较找到插入位置
//将该位置后的元素依次后移一个位置
//插入待插入的值

public class homework0207 {
    public static void main(String[] args) {
        int[] oldscores = {10,12,13,34,45};
        int[] newscores = new int[oldscores.length + 1];
        int num = 14;
        //复制旧数组的值到新数组中
        for (int i = 0; i <oldscores.length ; i++) {
            newscores[i] = oldscores[i];
        }
        //通过比较找到插入位置
        int index = 0;
        for (int i = 0; i < newscores.length ; i++) {
            if(num < newscores[i]){
                index = i;
                break;
            }
        }
        //将该位置后的元素依次后移一个位置
        for (int j = index; j < oldscores.length ; j++) {
            newscores[j + 1] = oldscores[j];
        }
        //插入待插入的值
        newscores[index]=num;

        //输出数组
        for (int a = 0; a <oldscores.length ; a++) {
            System.out.print(oldscores[a]+" ");
        }
        System.out.println();
        for (int b = 0; b < newscores.length; b++) {
            System.out.print(newscores[b]+" ");
        }
    }
}

1.6.5.2 冒泡排序

原理:比较两个相邻的元素,将值大的元素交换到右边

思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

    (1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

    (2)比较第2和第3个数,将小数 放在前面,大数放在后面。

    ......

    (3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

    (4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

    (5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

    (6)依次类推,每一趟比较次数减少依次

口诀

  • n个兄弟来排序
  • 两两比较小靠前
  • 外部循环n-i
  • 内部循环n-i-1

QQ图片20210516094037

int[] nums = {12,23,34,23,12}
for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - i -1; j++) {
                temp = nums[j];
                if (nums[j] > nums[j + 1]){
                    nums[j] = nums[j +  1];
                    nums[j + 1] = temp;
            }
        }
    }

1.6.5.3 选择排序

基于冒泡排序,解决冒泡排序的问题

实现思路:

首先在未排序的数列中找到最小或最大元素的位置,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。

int[] nums = {12,24,1,45,13};
        int temp;
        for (int i = 0; i < nums.length; i++) {
            //找到当前未操作的数组的最小元素位置
            int min = i;
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[min]){
                    min = j;
                }
            }
            //找到该位置后,要把最小元素移到数组的最前面
            if (min != i){
                temp = nums[i];
                nums[i] = nums[min];
                nums[min] = temp;
            }
        }
        for (int a = 0; a < nums.length; a++) {
            System.out.print(nums[a]+" ");
        }

冒泡排序与选择排序的区别

  • 冒泡排序是左右两个数相比较,而选择排序是用后面的数和每一轮的第一个数相比较;
  • 冒泡排序每轮交换的次数比较多,而选择排序每轮只交换一次;
  • 冒泡排序是通过数去找位置,选择排序是给定位置去找数;
  • 当一个数组遇到相同的数时,冒泡排序相对而言是稳定的,而选择排序便不稳定;
  • 在效率上,选择排序优于冒泡排序。

ps:两种算法虽然效率不一样,但是最终比较的次数是一样的

1.6.5.4 快速排序

快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在 1962 年提出。

基本思想:通过一趟排序将要排序的数据分为独立的两部分,其中一部分的所有数据比另一部分的所有数据都小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据编程有序序列。

先从数组中找一个数作为中轴数pivot,一般选数组最左边的作为pivot
然后从数组两边进行查找
       1.先从数组右边查找比pivot小的元素,如果查找到了,就停下
       2.再从数组左边查找比pivot大的元素,如果查找到了,就停下
       3.都停下后,交换这两个数
       4.然后继续查找,重复1-3的步骤
       5.最后把知道左右两边的位置重合,不再查找
       6.把pivot和重合位置的元素进行交换
       7.最后一轮排序结束,得出pivot左边的都是比pivot小的元素
          在pivot右边的都是比pivot大的元素
       
       
  然后,继续分别操作pivot左边的数组元素部分
               和pivot右边的数据元素部分 

1.6.6 查找算法

1.6.6.1 二分法查找(折半查找)

对一个排列有序的数组,进行元素查找的算法

基本思想

  • 每次先将数组拆分成两部分,然后在有可能包含待查找元素的那一部分中,接着查找,并根据判断结果继续拆分,逐步缩小查找范围,直到找到或找不到为止,查找时间复杂度log2n
  • min保存当前最小值的位置
  • max保存当前最大值的位置
  • mid保存当前中间值的位置mid=(min+max)/2
public class binarysearch {
    public static void main(String[] args) {
        int[] nums = {23,34,45,56,67,78,89,100};
        int number = 56;

        //初始化min,max,mid的位置
        int min = 0;
        int max = nums.length - 1;
        int mid = (min + max)/2;

        while(min <= max){
            //每次开始前,需要重新计算mid的位置
            mid = (min + max)/2;
            if (number < nums[min] || number > nums[max]){
                System.out.println("在数组中没有找到该数");
                break;
            }else if(number < nums[mid]){
                max = mid - 1;
            }else if (number > nums[mid]){
                min = mid + 1;
            }else{
                System.out.printf("%d在数组中的下标位置是%d",number,mid);
                System.out.println("该值为:"+nums[mid]);
                break;
            }
        }
    }
}

1.7 面向对象

1.7.1 面向对象概念

类的概念与对象的概念

  1. 对象的概念

对象:用来描述客观事物的一个实体,由一组属性和方法构成

    • 属性也叫字段,记录对象的数据
    • 行为也叫方法,描述该对象的动态特征,即它能作什么。
    1. 封装的概念

      • 对象同时具有属性和方法两项特性
      • 对象的属性和方法通常在一起,共同体现事物的特性, 二者相辅相承,不能分割,这种操作就叫做封装。
    2. 类的概念

      • 类是抽象的概念,是对现实世界一组相同的对象的特征的描述,是一种模板
      • 对象是具体的,是对类的具体的体现。

    1.7.2 定义类的步骤

    • 定义类名

    类名规则:

    帕斯卡规则:

    ​ 首字母大写,后续小写

    • 定义类的属性
    • 定义类的方法
    //演示一个学生概念类
    package com.iwebstudy.jerrywu.javaday3;
    
    /**
     * @author JerryWu
     * @date 2021/5/16
     * @desc 自定义学生类
     */
    public class student {
        //定义属性
        public int id;//学号
        public String name;//姓名
        public String borndate;//出生日期
    
        //定义方法(行为)
        public void sayhi(){
            System.out.printf("大家好,我叫%s,我的学号为%d",name,id);
        }
    }
    

    1.7.3 对象的创建和使用

     //对象的创建
            student student01 = new student();
            student student02 = new student();
    
            //为对象属性赋值
            student01.id = 18060121;
            student01.name = "JerryWu";
            student02.id = 1802312;
            student02.name = "tom";

    1.7.4 方法

    方法是用户描述类的某种行为或功能

    1.7.4.1 方法的定义

    定义类的方法分为两步:

    1. 定义方法名以及返回值类型
    2. 编写方法体
    //定义方法名以及返回值类型
    public 返回值类型 方法名(){
        //方法主题
    }

    示例1

    无参方法:学生自我介绍的方法

    public class Student{
        public int id;
        public String name;
        public void sayhi(){
            System.out.printf("大家好,我叫%s,我的学号为%d",name,id);
        }
    }

    示例2

    有参方法:榨汁机为例

    public class Jucier {
        public String name;//榨汁机名字
        public double volume;//榨汁机容积
    
        /**
         *榨汁方法
         * @param a 形参
         * @param b 形参
         */
        public void juicing(String fruit){
            System.out.printf("榨出了%s汁",fruit);
        }

    示例3

    带多个参数的方法,如加减乘除

    //带参方法,加法
        public void add(int a, int b){
            System.out.println(a + b);
        }

    PS:定义多个参数时,要根据当前需求,决定使用几个参数,参数与参数之间使用英文逗号隔开

    示例4

    有返回值的方法

    public class Juicer{
        public String brand;
        public int volume;
        public String juicing(String fruit){
            String juice = fruit + "汁";
            return jucie;
        }
    }

    PS:return的返回值的类型,要跟方法定义时的类型保持一致

    1.7.4.2 方法的调用

    1.7.4.2.1 无参方法调用
    Student student = new Student();
    student.id = 180103;
    student.name = "Alice";
    student.Sayhi();
    1.7.4.2.2 有参方法调用
    Juicer juicer = new Juicer();
    //先定义变量,再传值
    String fruit = "苹果";
    juicer.juicing(fruit);
    //直接传值
    juicer.juicing("苹果");

    PS:无论是通过变量传值,还是直接传值,都需要注意传入的值的类型,要跟方法定义时参数的类型一致。

    多个参数的方法调用时,需要注意参数的位置,不能穿错

    1.7.4.2.3 带返回值的方法的调用
    Juicer juicer = new Juicer();
    //先定义变量,再传值
    String fruit = "苹果";
    juicer.juicing(fruit);
    //直接传值
    String juice = juicer.juicing("苹果");

    PS:接收方法参数时,要注意方法返回值类型,使用变量保存返回值时,变量的类型要与返回值类型一致。

    对象属性未初始化的初始值

    局部变量未初始化编译无法通过

    通过类创建的对象的属性系统会自动进行初始化

    1.7.4.3 方法参数的传递

    java中,方法的参数传递,分为普通类型的参数传递(值传递),与应用类型的参数传递

    值传递,传入的是变量中保存的值

    示例:两个数的交换方法

    public void change(int a, int b){
        int temp = 0;
        temp = a;
        a = b;
        b = temp;
    }
    
    //方法调用、
    int numA = 10;
    int nuMB = 20;
    change(numA.numB);
    
    //通过方法进行的值交换无法实现功能
    //这是值传递

    引用传递,传递的是对象的引用(地址值)

    操作的其实是同一对象

    示例1:数组排序

    public void bubbleSort(int[] arr){
        int temp = 0;
        for(int i = 0;i < arr.length - 1;i L++){
            for(int j = 0;j < arr.length - i - 1){
                if(arr[j] > arr[j + 1]){
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }

    示例2:对象作为参数传递

    ​ 对象作为参数传递时,方法里,可以使用该对象的公有属性和方法

    //水果类
    public class Fruit{
        public String name;
        public void getName(){
            System.out.println("我是一个" + name);
        }
    }
    // 榨汁机的榨汁方法
    public String juicing(Fruit fruit){
        String juice = fruit.name + "汁";
        return juice;
    }
    
    
    // 参数传递
    Fruit f = new Fruit();
    f.name = "苹果";
    // 调用榨汁机的方法
    Juicer juicer = new Juicer();
    juicer.juicing(f);

    1.7.5 方法重载

    1.7.5.1 方法重载的定义

    在一个类中,有多个同名的方法,这些方法有以下特征

    • 方法名相同
    • 参数数据类型或参数个数不同
    • 与返回值,访问修饰符无关

    这类方法就叫做方法重载

    1.7.5.2 方法重载的意义

    以公司会计给员工发工资为例,不同级别的员工,计算工资的方法也不同

    public double calcEmpSalary(Emp emp){
        return emp.baseSalary+emp.perfSalary();
    }
    public double calcLeaderSalary(Leader leader){
        return leader.baseSalary+leader.perfSalary()+leader.teamSalary;
    }
    public double calcManagerSalary(Manager manager){
        return manager.baseSalary+manager.perfSalary()+manager.projectSalary;
    }
    

    以上代码,都是在计算公司的员工工资,只不过是员工的工种类型不同而已。

    如果现在增加新的工种的工资,则需要再定义不同名称的计算工资的方法。

    这样,调用者再调用这些方法时,时不容易记住这些方法名称的

    改用重载后的代码如下:

    public double calcSalary(Emp emp){
        return emp.baseSalary+emp.perfSalary();
    }
    public double calcSalary(Leader leader){
        return leader.baseSalary+leader.perfSalary()+leader.teamSalary;
    }
    public double calcSalary(Manager manager){
        return manager.baseSalary+manager.perfSalary()+manager.projectSalary;
    }
    

    使用重载优化代码之后,嗲用着只需要记住计算工资的方法名即可。

    调用时,只需要传入不同类型的参数值即可,这样系统会自动根据参数值的类型,找出相应的方法进行调用,从而避免了调用者需要记忆大量功能相同,但是方法名不同的问题

    1.7.5.3 方法重载的使用

    调用重载方法时,直接像使用普通方法那样调用即可,调用时需注意传入参数的类型

    //员工类
    public class Emp {
        public int baseSalary;
        public int perfSalary;
    }
    
    //leader类
    public class Leader {
        public int baseSalary;
        public int perfSalary;
        public int teamSalary;
    }
    
    //manager类
    public class Manager {
        public int baseSalary;
        public int perfSalary;
        public int projectSalary;
    }
    
    //accountant类
    public class Accountant {
        /**
         员工工资
         */
        public int calcEmpSalary(Emp emp){
            return emp.baseSalary+emp.perfSalary;
        }
    
        /**
         领导工资
         */
        public int calcLeaderSalary(Leader leader){
            return leader.baseSalary+leader.perfSalary+leader.teamSalary;
        }
    
        /**
         经理工资
         */
        public int calcManagerSalary(Manager manager) {
            return manager.baseSalary + manager.perfSalary + manager.projectSalary;
        }
    }
    
    //TestAccount
    public class TestAccount {
        public static void main(String[] args) {
            Emp emp = new Emp();
            emp.baseSalary = 3000;
            emp.perfSalary = 5000;
    
            Leader leader = new Leader();
            leader.baseSalary = 3000;
            leader.perfSalary = 6000;
            leader.teamSalary = 5000;
    
            Manager manager = new Manager();
            manager.baseSalary = 3000;
            manager.perfSalary = 6000;
            manager.projectSalary = 6000;
    
            Accountant accountant = new Accountant();
            System.out.println(accountant.calcEmpSalary(emp));
            System.out.println(accountant.calcLeaderSalary(leader));
            System.out.println(accountant.calcManagerSalary(manager));
        }
    }

    1.7.6 构造函数

    1.7.6.1 构造函数的定义

    对象通过new关键字创建

    通过一个叫做构造函数(构造方法)的方法创建出来的。

    通过反编译的字节码文件,可以看出,在源码中,多了一个与类名同名的方法,这个方法是系统自动帮我们加入的,目的就是为了通过它创建出对象

    构造方法:

    由系统默认提供,用于创建对象

    • 无返回值
    • 与类名相同
    • 默认无参数(可以指定参数)

    1.7.6.2 构造函数的自定义

    若需要一个狗狗类,我们希望创造出来的狗狗对象(比如金毛),出来的时候毛色就应该是黄色的,所以就可以这样定义:

    public class Dog{
        public String name;
        public String color = "黄色";
        // 省略其他属性和方法
    }

    这样我们在对象创建之前,就为狗狗对象的color属性值做了赋值,后续每创建一个狗狗对象时,颜色我们就不用再赋值了,这样,可以少编写很多重复的代码,当然,这是一种方式,我们其实有更好的方式。

    我们可以在类中自定义构造函数,我们可以在这里给对象的属性做初始化。

    public class Dog{
        public Dog(){
            this.color = "黄色";
        }
    public String name;
        public String color;
    // 省略其他属性和方法
    }
    

    我们推荐使用这种方式,这样,对象在创建的同时,也可以为对象的属性赋值了。我们在color属性前加了this.做修饰,意思是区别我们使用的是普通的变量,还是类的属性。

    this代表的是当前要创建的对象,在对象的内部,我们是通过thi.属性名来访问对象的属性。

    尤其是在一个方法中,当方法中的参数,和类的属性取名一样时,这样能更好的区分当前我们需要访问的时属性还是方法中的参数。

    public class Dog{
        public Dog(){
            this.color = "黄色";
        }
        public String name;
        public String color;
        
        public void setName(String name){
    // this访问属性,区别方法的参数
            this.name = name;
        }
        public void printColor(){
            String color = "黑色";
    // 打印的结果是多少?
            System.out.println(color);
         }
    }

    当我们在访问对象的内部的方法时,我们也可以用this.方法名进行访问。

    public class Calc{
        public int numA;
        public int numB;
        public int sum(){
           return this.numA + this.numB;
        }
        public double avg(){
        //double avg = (double)(this.numA+this.numB)/2;
        // 直接调用求和的方法
        double avg = this.sum()/2.0;
        return avg;
        }
    }

    当我们在上面Dog类中提供了自定义的构造函数后,系统就不会再给我们提供默认无参的构造函数,这 点一定要记住。

    1.7.6.3 构造函数的重载

    有时,我们对上面自定义的无参构造函数里提供的属性的赋值不太满意,比如,我们希望狗狗在创建的 时候,颜色由我们自已定义,那么我们就可以再提供一个带自定参数的构造函数,那么我们在创建狗狗 对象时,就可以使用这个自定义的带参数的构造函数,来创建对象了。

    当多个构造函数被定义后,我们就可以根据我们的需要,选择不同的构造函数来创建对象:

    public class Teacher {
        String teacherNo;
        String teacherName;
        String profession;
        String techerMethod;
    
        public Teacher(){
    
            //无参构造只需外部构造一次
           /* String teacherNo;
            String teacherName;
            String profession;
            String techerMethod;*/
        }
    
        public Teacher(String teacherNo){
            this.teacherNo = teacherNo;
    
            /*String teacherName;
            String profession;
            String techerMethod;*/
        }
    
        public Teacher(String teacherNo, String teacherName){
            this.teacherNo = teacherNo;
            this.teacherName = teacherName;
    
            /*String profession;
            String techerMethod;*/
        }
    
        public void SayHi(){
            System.out.printf("我叫%s,是一名教师,编号为%s,我所教的专业是%s。",teacherName, teacherNo, profession);
            System.out.println();
        }
    }
    

    1.7.6.4 构造函数的特点

    • 与类名相同
    • 没有返回值
    • 用于创建对象
    • 可以对类的对象的属性作初始化
    • 可以构成重载
    • 类中提供了自定义构造函数后,系统不会再提供默认无参构造函数

    1.7.7 变量作用域

    Java中的变量有:

    • 全局变量:也叫成员变量,作为类的属性定义的量
    • 局部变量:在方法中定义的变量
    • 块级变量:在代码块{}中定义的变量

    作用域:

    • 全局变量:作用域范围为整个对象
    • 局部变量:作用域范围为从变量声明时,到方法结束
    • 块级变量:作用域范围为在当前块中,从变量的声明时,到块的结束
    if(条件){
        int a = 10;
        System.out.println(a);//正确
    }
    System.out.println(a);//错误

    1.7.8 封装

    1.7.8.1 封装的概念

    封装,是面向对象的三大特征之一

    定义:将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。

    1.7.8.2 为什么使用封装

    封装的优势:

    • 只能通过规定方法访问数据
    • 隐藏类的实现细节
    • 方便加入控制语句
    • 方便修改实现

    以榨汁机类为例,没有封装的属性的缺陷

    Juicer juicer = new Juicer();
    juicer.volumn = -5; // 属性在外部可以随意访问与赋值,进行了不合理的赋值

    1.7.8.3 如何封装

    封装分为三步骤进行:

    1. 修改属性的可见性,改为private
    2. 创建公有的getter/setter方法,用于属性的读写
    3. 在getter/setter方法中加入属性控制语句,以便对属性值的合法性进行判断

    示例:

    public class Juicer{
        public String brand;
        private int volumn;
        public int getVolumn(){
            return this.volumn;
        }
        public void setVolumn(int volumn){
            if (volumn < 0) {
            this.volumn = 2; // 设置一个默认值
            System.out.println("容积值不能为负数");
        } else{
            this.volumn = volumn;
          }
        }
    // 省略其他方法。。。。。。
    }
    

    ps:熟悉封装以后,相关的代码,不需要手动编写,因为比较费时费力,代码可以结束idea等工具自动生成,我们只需要加入关键的逻辑判断性的语句即可。

    1.7.8.4 如何访问封装数据

    示例,演示封装后的使用方法

    uicer juicer = new Juicer();
    // 访问榨汁机对象的容积属性
    System.out.println(juicer.volume); // error, volume属性为私有的,无法直接访问
    System.out.println(juicer.getVolumn());
    // 给容积属性设置值
    juicer.volumn = -100; // error,无法直接操作
    juicer.setVolumn(-100); // 错误的值会被封装内部的逻辑代码过滤掉
    juicer.setVolumn(5); // 正确的值可以设置成功

    1.7.9 继承

    1.7.9.1 继承的概念

    继承,是面向对象的第二大特征

    含义:继承,是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。

    1.7.9.2 为什么使用继承

    假设有一个类:

    public class Cat{
        public String name; // 名称
        public double weight; // 体重
        public String breed; // 品种
        public void speak(){
    // 省略方法实现
    }
        public void sleep(){
    // 省略方法实现
        }
        public void eat(){
    // 省略方法实现
        }
        public void run(){
    // 省略方法实现
        }
        public void catchMouse(){
    // 省略猫抓老鼠的方法实现
        }
    }

    然后还有一个类:

    public class Dog{
        public String name; // 名称
        public double weight; // 体重
        public String breed; // 品种
        public void speak(){
    // 省略方法实现
        }
        public void sleep(){
    // 省略方法实现
        }
        public void eat(){
    // 省略方法实现
        }
        public void run(){
    // 省略方法实现
        }
    // 撒娇
        public void fawn(){
    // 省略狗狗撒娇的方法实现
        }
    }

    我们对比Cat类的定义和Dog类的定义,发现它们有大量相同的属性和方法,“不要重复造轮子”, 其实这 些重复的属性和方法,我们可以单独定义在一个类中:

    public class Animal{
    public String name; // 名称
    public double weight; // 体重
    public String breed; // 品种
    public void speak(){
    // 省略方法实现
    }
    public void sleep(){
    // 省略方法实现
    }
    public void eat(){
    // 省略方法实现
    }
    public void run(){
    // 省略方法实现
    }
    }

    优化后的Cat和Dog

    public class Cat{
    public void catchMouse(){
    // 省略猫抓老鼠的方法实现
    }
    }
    public class Dog{
    // 撒娇
    public void fawn(){
    // 省略狗狗撒娇的方法实现
    }
    }

    我们可以使用一种叫继承的技术,让Cat和Dog拥有Animal类中的所有属性和方法

    • Cat继承自Animal类,Dog也能继承Animal类
    • Animal是父类,Cat和Dog是子类
    • 子类和父类是一种is-a的关系,即,Cat是一种Animal,Dog也是一种Animal
    • 这种关系,也符合自然界的规律,一个子类只能继承一个父类img

    1.7.9.3 如何实现继承

    在Java中,实现继承,只需要在定义子类时,使用extends关键字继承父类即可:

    public class Cat extends Animal{
    public void catchMouse(){
    // 省略猫抓老鼠的方法实现
    }
    }
    public class Dog extends Animal{
    // 撒娇
    public void fawn(){
    // 省略狗狗撒娇的方法实现
    }
    }

    子类继承父类后,就可以使用所有父类的公有资源了

    1.7.9.4 继承的应用

    子类继承了父类之后,子类就可以像使用自己的属性和方法那样使用从父类继承的属性和方法

    Dog dog = new Dog();
    dog.name="大黄"; // name是继承父类的属性
    dog.eat(); // eat()是继承父类的方法

    在子类方法的内部,也可以使用super手动指定父类的成员

    // 撒娇
    public void fawn(){
    // 省略狗狗撒娇的方法实现
    super.name = "大黄";
    }

    ps:还有需要用到super的地方,自定义构造函数

    1.7.9.5 方法重写

    当狗狗对父类的方法eat不满意时,比如,狗狗吃东西时,有自己特有的方式,父类继承给Dog的eat()方法就不适合了,那么此时该怎么办?

    其实我们的子类,如果不满意父类继承的方法时,我们不需要再定义一个方法,我们只需要去重写(覆盖)父类所继承给子类的那个方法即可:

    public class Dog extends Animal{
    public void eat(){
    System.out.println("这是狗狗自己吃东西的方法。");
    }
    // 撒娇
    public void fawn(){
    // 省略狗狗撒娇的方法实现
    }
    }

    上述代码就完成了重写的操作,在重写时,除了方法主体不一样,其他的必须跟父类的相应方法保持一致,如果不一致,则会重写失败,成为一个子类的新的方法。

    1.7.9.6 访问修饰符

    子类可以继承父类的所有资源吗?

    不能被继承的父类成员:

    • private成员
    • 子类与父类不在同包,使用默认访问权限的成员
    • 构造方法

    访问修饰符protected

    • 可以修饰属性和方法
    • 本类、同包、子类可以访问

    访问修饰符权限范围:

    访问修饰符本类同包子类其他
    private
    默认(friendly)
    protected
    public

    1.7.9.7 Object类

    在java中,一切皆对象, 一切类其实都继承自一个基类 Object。

    在上述继承关系中,Animal继承自Object, Dog继承自Animal, 继承可以传递继承,所以,Dog类虽然只 继承了Animal类,但它同样可以获取到爷爷辈Object所提供的所有的公有的资源,如toString()方法。

    1.7.9.8 最终类

    在java中 使用final修饰的类,是最终类

    这种类,不能再派生子类

    最终类的特点,系统提供的最终类如String、Math。

    1.7.10 抽象

    1.7.10.1 抽象的概念

    通过abstract关键字声明的类,叫做抽象类

    public abstract class 类名{
        
    }
    • 一个类被声明为抽象类后,不可以在外部被实例化
    • 抽象类中,可以包含抽象方法和普通方法
    • 一个类如果包含了抽象方法,则该类必须是一个抽象类

    1.7.10.2 为什么需要抽象

    前面讲到的Dog、Cat、Rabbit和Mouse类及其它们的父类Animal类,我们在具体使用时,通常不会特意实例化父类Animal,而是去实例化具体的某个子类对象。所以下面的实例化通常就没有实际意义了。

    Animal animal = new Animal();

    所以我们可以把这种没有意义的实例化去掉,让Animal这个父类变为抽象的一种概念,在外部不能对其实例化。

    public abstract class Animal{
        //省略属性和方法
    }

    Animal类被抽象化以后,我们继续思考它里面的方法,eat(),speak(),run(),sleep()这些方法,其实都在描述一种动物它应该所具备的一些行为特征,其实也是一种抽象的概念。把这些概念性的,但是又包含具体方法实现的方法,继承给我们的子类后,这些子类往往时用不上的,有时为了正确使用,不得不去重写它们,而如果忘记重写它们,在调用时,又会调用到这些不太有意义的方法。

    怎么解决呢?

    我们就可以使用抽象方法进行解决。

    我们将抽象的父类中的那些对子类很重要的方法,但是又不需要父类去定义方法主体的方法,定义成抽象方法。

    这样,子类继承了这个父类后,就必须要重写这些方法,才能使用。

    从面向对象的角度看,一个抽象的父类,其实是对它们子类的一种行为上的约束,即,你是一只动物,则,你必须要会eat(),会sleep()......,这样,才是一只合格的动物。

    public abstract class Animal{
        public abstract void run();
    }

    1.7.10.3 如何实现抽象

    1. 实现抽象类

    在普通的继承关系中,父类派生出子类,子类继承父类,在抽象的继承关系中,子类也是要“继承”抽象的父类的,只是这是我们要换种描述,是子类实现抽象的父类。子类实现抽象类,也是通过extends关键字。

    public class Dog extends Animal{
        
    }
    1. 实现抽象方法

    当子类实现了抽象类之后,该子类必须重写父类的所有的抽象方法,以实现概念,从概念到具体。

    public clas Dog extends Animal{
        
        @override
        public void run(){
            //子类实现父类的抽象方法
        }
    }

    1.7.11 多态

    1.7.11.1 多态的概念

    多态,面向对象的第三大特征

    以生活中的多态举例:

    打印机:不同类型的打印机,打印的效果不同。有彩色打印机和黑白打印机

    同一种操作,由于条件不同,产生的结果也不同

    程序中的多态的概念:同一个引用类型,使用不同的实例而执行不同操作

    1.7.11.2 为什么要使用多态

    例如:

    张三现在老了,孩子们长大了,都不在身边,为了打发无聊的时光,张三打算养一只猫和一条狗。每天张三都要给他的小动物们喂食。先用程序模拟:

    public class OldMan{
        //省略属性
        
        //喂养狗狗
        public void feedAnimal(Dog dog){
            dog.eat();
        }
        
        //喂养猫猫
        public void feedAnimal(Cat cat){
            cat.eat();
        }
    }

    在OldMan中,我们使用了重载,进行了相关代码的定义。但是这样的定义不好,比如后期,张三又养了一只兔子和一只鸡,那么我们又要针对喂养兔子和喂养鸡添加不同的方法,这样势必会

                        **频繁修改代码,代码可扩展性、可维护性差**

    那么如何优化代码,以提高扩展性和维护性呢?

    以上重载方法的参数,都是Animal的子类,其实都是在进行喂养的行为,那么能否使用一个feedAnimal(Animal animal)方法,以实现能喂养所有的动物呢?

    这就是多态要做的事!

    1.7.11.3 如何使用多态

    public class OldMan {
    // 省略属性
    // 喂养动物, 多态方法的定义
    public void feedAnimal(Animal animal){
    animal.eat();
    }
    }
    
    // 简易版main方法
    main(){
    // 子类对象保存到父类类型中
    Animal dog = new Dog();
    Animal cat = new Dog();
    OldMan zhangsan = new OldMan();
    zhangsan.feedAnimal(dog);
    zhangsan.feedAnimal(cat);
    }
    

    1.7.11.4 里氏代换原则

    里氏代换原则LSP,由麻省理工学院计算机科学实验室理斯科夫(Liskov)女士在1987年提出来的:继承必须确保超类所拥有的性质在子类中仍然成立

    LSP原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该 使用继承,以及其中蕴含的原理。里氏替换原则是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

    里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

    根据上述理解,对里氏替换原则的定义可以总结如下:

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 子类中可以增加自己特有的方法
    • 当子类的方法重载父类的方法时,方法的前置条件按(即方法的输入参数)要比父类的方法更宽松
    • 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等

    通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

    如果违背了里氏替换原则,则继承类的队形在基类出现的地方会出现运行错误,这是其修正方法是:取消原来的继承关系,重新设计它们之间的关系。

    1.7.12 常用设计模式

    1.7.12.1 UML类图

    泛化-继承: B继承自A B extends A

    image-20210603213847390

    实现: A为接口 B实现了A

    image-20210603214357398

    依赖: B 依赖于A

    image-20210603214655303

    关联: 类B使用类A,A作为B的成员变量,B单向关联A

    image-20210603214833023

    ​ 双向关联

    image-20210603215000312

    聚合:

    image-20210603215110268

    组合:

    image-20210603215145197

    1.7.12.2简单工厂

    以简单的计算器的案例为例进行讲解,需掌握UML类图

    需要表示的关系有:依赖、泛化-继承、实现、关联、聚合、组合

    依赖:类A当中使用了类B,其中类B是作为类A的方法参数、方法中的局部变量、或者静态方法调用。

    关联:

    ​ 单向关联表现为:类A当中使用了类B,其中B作为类A的成员变量。

    ​ 双向关联表现为: 类A当中使用了类B作为成员变量;同时类B中也使用了类A作为成员变量。

    聚合:聚合关系是关联关系的一种,耦合度强于关联,他们的代码表现是相同的,仅仅是在语义上有所

    ​ 区别:关联关系的对象间是相互独立的,而聚合关系的对象之间存在着包容关系,他们之间是“整体-个 体”的相互关系。

    组合:相比于聚合,组合是一种耦合度更强的关联关系。存在组合关系的类表示“整体-部分”的关联关 系,“整体”负责“部分”的生命周期,他们之间是共生共死的;并且“部分”单独存在时没有任何意义。

    讲解简单工厂时,先从最简单代码讲解,然后一步步解耦出简单工厂的模式。

    //运算类抽象类
    public abstract class Operator {
    public int numA;
    public int numB;
    //获取结果
    public int getResult();
    }
    
    public class AddOperator extends Operator{
    public int getResult(){
    return numA+numB;
    }
    }
    public class SubOperator extends Operator{
    public int getResult(){
    return numA-numB;
    }
    }
    public class MulOperator extends Operator{
    public int getResult(){
    return numA*numB;
    }
    }
    public class DivOperator extends Operator{
    public int getResult(){
    return numA/numB;
    }
    }
    
    //简单工厂类
    public class SimpleFactory {
    // 这里包含一个static的知识点,可以先从普通方法进行讲解
    // 再讲解static的用法
    public Static Operator createOperator(String type) {
    Operator opr = null;
    switch(type){
    case "+":
    opr = new AddOperator();
    break;
    case "-":
    opr = new SubOperator();
    break;
    case "*":
    opr = new MulOperator();
    break;
    case "/":
    opr = new DivOperator();
    break;
    }
    return opr;
    }
    }
    
    // 简易版main()
    main(){
    String type = "+";
    Operator opr = SimpleFactory.createOperator(type);
    opr.numA = 10;
    opr.numB = 20;
    int result = opr.getResult();
    }
    //程序要求    高内聚  低耦合

    1.7.12.3 工厂方法模式

    • 工厂方法模式,由父类工厂提供了创建产品的标准(抽象方法),再由不同的子类工厂去实现。
    • 工厂方法模式,将创建具体产品对象的动作,延迟到了子类工厂去创建。

    后续要添加新的运算法则,只需要去扩展具体的子类工厂,再由子类工厂创建出具体的具体的运算法则 对象即可。它不像简单工厂那样,当需要创建新的子类产品时,必须要去修改工厂类中具体的业务方法。

    见gitee中javaday5相关代码

    PS: 工厂方法目前的问题: 工厂方法中,客户端需求变化后,新的耦合度问题又产生了,如何解决?

    1.7.12.3.1 工厂方法的优化
    • 使用配置文件+反射模式优化工厂模式
    1. 客户需求,保存在配置文件中,需求变更了,直接改配置文件即可
    2. 封装工具,读取配置文件内容

      public String fectchClassName() {
      try {
      Properties p = new Properties();
      InputStream inputStream =
      Client.class.getClassLoader().getResourceAsStream("config/config.properties"
      );
      p.load(inputStream);
      String content = p.getProperty("className");
      return content;
      } catch (IOException e) {
      e.printStackTrace();
      return null;
      }
      }
    3. 使用反射加载对象

    Class.forName() 将类的.class文件加载到jvm中,并对类进行解释,执行类中的static块。

    OperatorFactory factory = null;
    String className = client.fectchClassName();
    // 使用反射通过完全限定名加载对象
    factory = (OperatorFactory) Class.forName(className).newInstance();
    // 创建具体的子类工厂
    Operator operator = factory.createOperator();

    1.7.12.4 抽象工厂模式

    某公司现在向计算器工厂进行定制,要求制造出苗星仁主题的计算器。过了一段时间,又有另一家公 司,要求定制钢铁侠主题的计算器,又过了一段时间。。。。。 像这种批量制作具体产品的情况,使用简单工厂,工厂方法模式,显然是无法实现的。我们此时,可以 使用抽象工厂。

    抽象工厂,Abstract Factor

    • 提供一个创建一系列相关或者相互依赖的对象的接口,而无需指定它们具体的类,批量创建产品族 的操作,则交给不同的子类实现。

    image-20210605132117531

    1.7.13 接口

    1.7.13.1 什么是接口

    接口是一种特殊的类型,通常只包含抽象方法,表示的是一种能力,常常用于扩展类的行为。 语法:

    public interface MyInterface {
    void foo(); //所有方法都会默认加上public abstract
    //其他方法
    }
    

    1.7.13.2 接口的特点

    • 接口不可以被实例化
    • 实现类必须实现接口的所有方法
    • 实现类可以实现多个接口
    • 接口中的变量都是静态常量
    • 接口中可以包含静态方法和普通方法,但普通方法必须是default权限

    ps:传统的理解是接口只能是抽象方法。但是程序员们在使用中,发现很不方便,实现接口必须重 写所有方法,很麻烦。所以java设计者妥协了,在java8中,支持default和static方法,这样,实 现接口时,可以选择不对default修饰的方法重写。

    1. 接口提供一个默认实现的方法,并且不强制实现类重写此方法
    2. 默认方法使用default关键字来修饰
    3. 在接口种被default标记的方法为普通方法,必须要写方法体。
    4. 接口中支持定义静态方法,将关键字换成static即可

    1.7.13.3 为什么使用接口

    小花的闺蜜要去投奔她的男朋友了,闺蜜住的次卧才清空不久,房东马上找到了新的租客,是一个带着 眼镜的男生,穿着格子衫,背着一个大背包。小花很生气,房东都不跟她商量,就直接把次卧租给了一 个男租客,而且,看到自己主卧的那个单薄的木门,便吵着找房东要说法。最后房东答应小花,给她的 主卧的门上装上一个防盗锁。。。。

    因为原来房间的木门,上面只有一个普通拉手,只能开门关门。

    现使用代码如何定义出换了防盗锁的门呢?

    由于门的种类很多,我们很快就能快速定义父类们与木门子类

    image-20210605132558652

    由于锁是独立的,则我们需要再定义一个独立的锁类,然后我们试着让木门再去继承这个锁类。

    image-20210605132620412

    但是java中只有单继承,没有多继承的概念。

    另外,即使木门继承了锁,其结果也很奇怪,违背了 子类 is a 父类 的原则(木门是一种锁)。如何解决这种场景?其实,小花的意思很简单,她觉得跟一个男租客一起租房,感到很不安全,想让她的木门能更安全点, 能锁上后,只有她能打开。

    所以,我们可以使用前面介绍的接口,来赋予这个木门的防盗的的能力,即木门能够被锁上与开锁。

    1.7.13.4 如何使用接口

    使用接口的步骤;

    1. 定义一个接口

      public interface Lockable {
      void lockUp();
      void openLock();
      }
      
    2. 让木门实现这个接口

      public class WoodenDoor extends Door implements Lockable{
      // 省略实现父类的方法
      // 实现接口的方法
      public void lockUp(){
      //省略方法主体的代码
      }
      public void openLock(){
      //省略方法主体的代码
      }
      }
    3. .现在,如果这木门如果希望能有一个门铃的功能,只需要再定义一个门铃接口,让木门实现就可以 了。

      public interface Ringable {
      void ring();
      }
      public class WoodenDoor extends Door implements Lockable, Ringable{
      // 省略实现父类的方法
      // 实现接口的方法
      public void lockUp(){
      //省略方法主体的代码
      }
      public void openLock(){
      //省略方法主体的代码
      }
      // 实现门铃的响声
      public void ring(){
      System.out.println("叮咚,门铃响了,可能是隔壁的在敲门~~~");
      }
      }

    1.7.13.5 接口和抽象类的对比

    • 接口表示一种能力,是对类的行为的一种扩展

      • 体现在接口的方法上
      • 面向接口编程,关心的是实现类有何能力,而不关心实现细节
      • 面向接口考虑的是一种约定而不考虑接口的具体细节
    • 抽象类,主要是为了约束子类的行为
    • 普通类与父类, 是 is a 的关系;与接口是 has a 的关系

    1.7.14 内部类

    在java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括四种:普通(成员)内部类、局部内部类、匿名内部类和静态内部类。

    1.7.14.1 普通(成员)内部类

    定义在另一个类的内部,形式如下

    public class Circle {
        private double radius = 3.5;
    
        //内部类
        class Draw {
            public void drawSahpe() {
                System.out.println("drawshape"+radius);
            }
        }
    • 类Draw像是类Circle的一个成员,Circle被称为Draw的外部类。
    • 内部类可以无条件的访问外部类的所有属性和方法(包括静态成员和私有成员)。
    • 当内部类拥有和外部类同名的属性或方法时,会发生隐藏现象,即默认情况下访问的是内部类的成员。

      • 如果要访问外部类的成员,则需要使用以下形式访问:

        public class Circle {
            private double radius = 3.5;
        
            //内部类
            class Draw {
                private double radius = 2.5;
                public void drawSahpe() {
                    System.out.println("drawshape"+radius);
                    //访问外部类的方式
                    System.out.println("drawshape"+Circle.this.radius);
                }
            }
        }
    • 在外部类中,是无法直接访问内部类的成员的,若要访问,必须先创建一个内部类的对象,再通过该对象访问。

      • 定义创建内部类的方法

        public class Circle {
            private double radius = 3.5;
        
            // 通过方法创建内部类对象
            public Draw getDrawInstance() {
                return new Draw();
            }
            
            public void print(){
            // 访问内部类对象的成员
            this.getDrawInstance().drawSahpe(); 
            }
            
            public void test(){
                Draw draw = new Draw();
                draw.drawSahpe();
            }
        
        
            //内部类
            class Draw {
                private double radius = 2.5;
                public void drawSahpe() {
                    System.out.println("drawshape"+radius);
                    //访问外部类的方式
                    System.out.println("drawshape"+Circle.this.radius);
                }
            }
        }
      • 外部访问内部类成员

          public static void main(String[] args) {
                Circle circle = new Circle();
        
                circle.test();
              
              //circle.print();
            }

    1.7.14.2 局部内部类

    • 定义在一个方法或一个作用域里面的类
    • 它和成员类的区别在于,局部内部类的访问,仅限于该方法或作用域内。
    • 局部内部类就像方法里的一个局部变量一样,不能又public、protected、以及static修饰。
    public class Person{ }
    public class Man{
    public Person getWoman(){
    //局部内部类
    class Woman extends Person{
    int age =0;
    }
    return new Woman();
    }
    }

    1.7.15 递归

    • 一种特殊的循环,表示在方法主体中调用方法,这是一种特殊的循环。

    示例1:n的阶乘

    • 5! = 1 x 2 x 3 x 4 x 5
    • 5! = 5 x 4 x 3 x 2 x 1
    • n! = 1 x 2 x 3 x 4 x 5......(n-1) x n
    • n! = n x (n-1).....3 x 2 x 1
    • n! = n x (n-1)!
    • 定义 0! = 1

    示例代码:

    long getFactorial(int n){
    if(n==0){
    return 1;
    }
    return n * getFactorial(n-1);
    }

    示例2:斐波那契数列

    斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家莱昂纳多·斐波那契 (Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数 列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定 义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)

    • 第0项是0,第1项是第一个1。
    • 这个数列从第三项开始,每一项都等于前两项之和。
    public static int getFibonacci(int n){
        if (n == 0 || n == 1){
            return n;
        }
        return getFibonacci(n-1)+getFibonacci(n-2);
        }

    递归的优劣

    优点

    • 代码简介
    • 易于理解

    缺点

    • 时间与空间消耗大

    递归由于是函数调用自身,而函数的调用是消耗时间和空间的,每一次函数调用,都需要在内存栈中分配空间以保存参数,返回值和临时变量,而往栈中压入和弹出数据也都需要时间,所以降低了效率。

    • 重复计算

    递归中有很多计算是重复的,其本质是把一个问题分解成多个小问题,而多个小问题存在重叠的部分,即存在重复计算的部分,比如上述的斐波那契的递归实现。

    • 调用栈溢出

    递归可以使调用栈溢出,因为每次调用时,都会在栈中分配空间,而栈空间容量是有限的,当调用次数太多,就有可能超出栈的大小,从而导致内存的溢出(stack overflow)(不是内存泄漏)。

    内存溢出

    • OOM(out of memory)
    • 指存储的数据超出了指定空间的大小,这时数据就会越界,举例来说,常见的溢出,是指在栈空间 里,分配了超过数组长度的数据,导致多出来的数据覆盖了栈空间其他位置的数据。

    内存泄漏

    • 内存泄露本意是申请的内存空间没有被正确释放,导致后续程序里这块内存被永远占用(不可达)
    • 当且指向这块内存空间的指针不再存在时,这块内存也就永远不可达了,内存空间就这么一点点被 蚕食 借用一经典比喻:比如有10张画板,本来一人一张,画完自己擦了需要还回去,别人可以继续画, 现在有个家伙拿了画板不擦不还,然后还跑了找不到人了,如此就只剩下9张画板给别人用了,这 样的人多起来后,最后大家一张画板都没有了。

    1.7.16 jvm内存划分-低配版2.0

    jvm在运行时,数据区域通常划分为:堆、栈、本地方法栈、程序计数器、方法区

    image-20210617222821245

    1.7.16.1 程序计数器

    程序计数器,也叫PC寄存器,用于存储指向下一条指令的地址,即程序将要执行的指令代码。由执行引擎读取下一条指令。

    • 程序计数器是一块很小的,运行速度最快的内存空间
    • 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
    • 它是程序执行流程的指示器,分支、循环、跳转等基础功能都要依靠这个计数器完成
    • 它是唯一一个在java虚拟机规范中没有OutOfMemoryError情况的区域

    示例

    main(){
    int a=10;
    int b=20;
    int c=a+b;
    }

    通过javap -v Test.class反编译这个程序的字节码文件, 理解程序计数器的作用:

    image-20210617223602796

    可以看到上述代码,左侧的0-10代表着每条指令的指令地址,右侧的就是指令。

    image-20210617223627771

    1.7.16.2 虚拟机栈

    虚拟机栈,简称java栈,主管程序的运行,保存方法的局部变量、部分结果,并参与方法的调用与返回。

    由于基于跨平台的设计,Java的指令,都是根据栈进行设计的。

    栈解决程序运行的问题,程序如何执行,怎么处理数据。

    堆解决数据存储的问题,数据怎么存,存哪里。

    每个线程,都会创建一个虚拟机栈(是线程私有的),其内部保存一个个的栈帧,对应着一次次的java的方法调用。

    image-20210617224626378

    栈的特点

    • 是一种快速有效的分配存储的方式,访问速度仅次于程序计数器。
    • jvm对于栈的操作只有两种,方法执行时,进栈,方法执行结束后,出栈。
    • 没有垃圾回收,但可能会OOM

      • StackOverFlow、outofmemory
    • FIFO(first in first out)、LIFO(last in first out)

    1.7.16.3 本地方法栈

    本地方法栈,用于管理本地方法的调用,线程私有

    本地方法,是指使用C语言实现的方法

    1.7.16.4 方法区

    方法区,是被线程共享的区域。jvm在装载类文件时,用于存储类型信息(类的描述信息)

    • 方法区存储的信息包含:

      • 类的限定名
      • 类的直接父类的限定名
      • 该类是类型还是接口类型
      • 类的访问修饰符
      • 直接超接口的全限定名的有序列表
    • 已加载的类的详细信息

      • 运行时常量池:在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量。常量池中存储了诸如文字字符串、final变量值、类名和方法名常量
      • 字段信息:字段信息存放类中声明的每一个字段的信息,包括字段的名称、类型、修饰符。
      • 方法信息:类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、访问修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区)
      • 静态变量:除了常量意外的所有类(静态)变量,类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问,这些变量只与类有关——而非类的实例,因此他们总是作为类型信息的一部分而存储在方法区,除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中味这些类变量分配空间。

      而编译时常量(就是那些用final声明以及在编译时已知的的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会赋值它的所有常量到自己的变量池中,或嵌入到它的字节码流中,作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样,但是当一般的类变量作为声明他们的类型的一部分数据保存的时候,编译时常量作为使用它们的类型的一部分而保存。

      • 到类classloader的引用:到该类的类装载器的引用
      • 到类class的引用:虚拟机为每一个被装载的类型创建一个class实例,用来代表这个被装载的类。

      这个class实例,简单理解为:该类的原型:类模板对象。

      所谓类模板对象,其实就是java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段,类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取java类中的任意信息,能够对java类的成员变量进行遍历,也能进行java方法的调用。

      反射的机制是基于这一基础。如果JVM没有将java类的声明信息存储起来,则JVM在运行期也无法反射

      class实例对象就类似spring中的BeanDefinition,封装了bean的各种信息,也作为访问bean的入口。

    1.7.16.5 GC回收机制

    GC回收机制 ---- 守护线程

    负责回收失去了引用的对象 所占据的内存空间

    无法确定回收机制

    程序员也无法主动调用GC机制来回收

    因为有GC机制,java不存在内存泄漏,但是存在java特色的内存溢出

    1.7.16.6 static关键字

    java中,static可以修饰变量、方法、代码块

    静态变量

    • 普通成员变量(属性),需要实例化对象以后才能使用
    • 静态变量,不属于某个类的实例,而是属于类,所以也称为类变量。
    • 只要程序加载了类的字节码,不需要创建类的实例,静态变量就会被分配空间,然后就可以被访问了
    • 故,有的属性,为了提高它们的访问级别,可以将其设置为静态变量。

    如Person类

    public class Person{
        public String name;
        public static String county="china";
    }

    静态方法

    通过关键字修饰的方法,称为静态方法

    • 静态方法会随着类的定义而被分配和装载在内存中
    • 非静态方法属于对象的具体实现,只有在类的对象创建时,在对象的内存中才有这个方法的代码段。

    如:

    public class Person{
        public static void hello(){
            
        }
    }

    访问规则

    在java中,静态成员变量,一般要求通过类名.属性名的方式访问。

    静态方法一般要求通过类名.方法名的方式访问

    普通方法、静态方法、静态成员变量交叉访问的情况下,规则如下:

    访问规则普通成员变量静态成员变量普通方法静态方法
    普通方法直接访问
    或this.属性名
    直接访问
    或类名.属性名
    直接访问
    或this.方法名()
    直接访问
    或类名.方法名()
    静态方法不能直接访问
    除非先实例化对象
    再通过对象访问
    直接访问
    或类名.属性名
    不能直接访问
    除非先实例化对象
    再通过对象访问
    直接访问\
    或类名.方法名()

    1.7.16.7 final关键字

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

    final作用
    修饰变量属性值不希望被修改
    修饰方法方法不希望被子类重写
    修饰类类不希望被其他类继承

    PS:static可以和final联合进行属性和方法的修饰

    面试题:

    public class TestA {
    public TestA(){
    System.out.println("TestA()");
    }
    static {
    System.out.println("static");
    }
    // public final static String HELLOWORLD = "hello world";
    public static String HELLOWORLD = "hello world";
    }
    class TestB {
    public static void main(String[] args) {
    System.out.println(TestA.HELLOWORLD);
    }
    }

    以上问题,涉及两个问题

    • 类什么时候被加载
    • jvm加载final修饰的变量时,类是否被加载

    1.7.17 类的加载

    java中类的生命周期,大致分为五个阶段:

    image-20210618194805898

    1. 加载:查找获取类的class文件
    2. 连接:把class文件加载到java虚拟机
    3. 初始化:在虚拟机中根据class文件进行初始化
    4. 使用
    5. 卸载:使用完了,java虚拟机进行清理

    我们经常谈论的类的加载,并不是单指类的生命周期中的加载,而是类的生命周期中的加载、连接和初始化的三个阶段。在加载阶段,java虚拟机会做什么工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后再堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

    1.7.17.1 类什么时候被加载

    测试方法,使用-XX:+TraceClassLoading监控类的加载

    image-20210618195251918

    1. 定义了main方法的类,在启动main()时,该类会加载
    2. 创建了类的实例时,类会加载

    PS1:当一个类在虚拟机生命周期中存在时,则不会再被加载

    PS2:在有继承关系的情况下,实例化子类对象前,先加载父类,再加载子类

    1. 访问静态方法时,类会加载
    2. 访问类的静态变量时,类会加载
    3. 使用反射Class.forName()时,类会加载,默认情况下,不创建类的实例

    PS:注意点

    • 再JVM生命周期中某个类如果存在,则不会重复加载
    • 在加载子类的时候会优先加载 其父类。
    • 类被加载的时候,其中的静态代码块,静态方法及静态变量也会被加载
    • 在初始化某个类时,如果这个类的静态代码块,静态方法或静态变量引用到了另一个类,则这个类也会被加载
    • java中还有一种加载类的方式ClassLoader,二者区别:

      • Class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块,当然还可以指定是否执行静态块
      • ClassLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance()才回去执行static块
    1. 当final来搅局

    类中有final修饰的静态常量时,当访问该常量时:

    • 编译时就可以确定该值,访问时,无需加载该类
    • 运行时才能确定该值,访问时,需要加载该类

    1.7.18 系统常用类

    1.7.18.1 String字符串类型

    不是基本类型,本质上是引用数据类型;
    被final修饰,不能被继承,不可变对象,底层实现为char数组

    // String的创建
    // 通过=直接赋值
    String a = "a";
    // 通过构造函数创建
    String b = new String("b");
    // 通过+号连接,进行字符串拼接
    String c = a + b;

    常量池技术:所有通过等号直接赋值的字符串,并不会存放在堆内存中,而是会存放再常量池中
    ​ 若常量池中已有对象,则在使用等号进行直接赋值时,不会创建新的对象

    String aa = "a";
    String bb = "b";
    String cc = "ab";
    String dd = a + b;
    String ee = "a" + "b";
    System.out.println(cc == dd);
            // 结果为false
            // 在+号连接的底层实现过程中,创建了新的字符串对象
    System.out.println(cc == ee);
        // 结果为true
        // 如果字符串拼接时,左右两边都是直接的字符串,而不是字符串的引用,JVM在编译期间会直接将其拼接起来
    
        // ==比较的是地址,可能会出现字符串相同,地址不同的情况
        // 此时使用equals方法  做值的比较
    1.7.18.1.1 字符串常用方法
    String str = "abcde";
    // 1.equals  字符串比较
    // 2.charAt  获取字符
    System.out.println(str.charAt(1));     //打印为b
    // 3.toCharArray  将字符串对应转换成对应的char数组并返回
    char strs[] =str.toCharArray();
    System.out.println(strs[1]); 
    // 4.subString     做字符串截取
    System.out.println(str.substring(2));    // 从下标为2的字符开始截取,打印为cde
    System.out.println(str.substring(2,3));    // 从下标为2的字符开始截取,到下标为3结束,打印为c
    // 5.split   分隔
    String str2 = "abc cba bac bca";
    String strs[] = str.split(" ");
    for (String s:strs) {
          System.out.println(s);
       }
        
    // 6.trim    去掉首尾空格
     String a = "    abs  sdf    ";
     System.out.println(a.trim());
        
    // 7.toLowerCase   转换为小写
    String a = "TTTTTT";
    System.out.println(a.toLowerCase());   // 打印为tttttt
        
    // 8.toUpperCase   转换为大写
    String b = "tttttt";
    System.out.println(b.toUpperCase());   // 打印为TTTTTT
        
    // 9.indexOf    lastIndexOf    contains定位
    String str = "abcdebg";
    System.out.println(str.indexOf("b"));         //打印1,第一个对应字符的位置的下标
    System.out.println(str.lastIndexOf("b"));//打印5,最后一个对应字符的位置的下标
    System.out.println(str.contains("b"));        //打印boolean,是否含有指定字符
        
    // 10.replaceAll    replaceFirst   替换
    String str = "aaabcde";
    System.out.println(str.replace('a','A'));         //替换全部指定字符
    System.out.println(str.replaceFirst("a", "A"));  //替换第一个指定字符
        
    // 11.startsWith   endsWith
    String str = "ab000ba";
    System.out.println(str.startsWith("ab"));
    System.out.println(str.endsWith("ba"));
        
    // 字符串格式化
    String str1 = "%s已经被揍了%d次,他大声的说到:%s";
    String str2 = String.format(str1, "ppy", 10, "就这?");
    System.out.println(str2);

    1.7.18.2 StringBuffer可变字符串

    // 线程安全,可变的字符序列
        // stringBuffer的创建
        // 只支持new方法
        StringBuffer stringBuffer = new StringBuffer();
    
        // StringBuffer的主要StringBuffer是append和insert方法,它们被重载以便接受任何类型的数据。
        // 每个都有效地将给定的数据转换为字符串,然后将该字符串的字符附加或插入到字符串缓冲区。
    
        // StringBuffer本身的存储结构是char数组
        // 每当值发生变化 会根据值的增长 对数组的长度进行扩容
        // 将原有的数组元素 复制到新数组中
        // StringBuffer在创建一个空对象时,初始容量为16
        // 创建一个非空对象时,初始容量为16+n(字符串长度)
        // 在对象的长度超过16之前,都不需要更换新的容易
        // StringBufffer 的容量会在达到上限的时候进行扩容
        // 扩容规则:当前容量上限+1再*2
        // 空间换时间
    
        // StringBuffer 的类型转换
        // sb转String
        String str = stringBuffer.toString();
        String 转StringBuffer
        StringBuffer stringBuffer = new StringBuffer(str)
             
        // 1.append方法总是在缓冲区的末尾添加这些字符;
        StringBuffer stringBuffer = new StringBuffer("abcdef");
        stringBuffer.append("添加部分");
        System.out.println(stringBuffer);
    
        // 2.insert方法将insert添加到指定点。
        StringBuffer stringBuffer = new StringBuffer("abcdef");
        stringBuffer.insert(3,"插入部分");    //在第n个字符后插入指定部分
    
        // 3.delete
        StringBuffer stringBuffer = new StringBuffer("abcdef");
        stringBuffer.delete(3,5);
    
        // 4.reverse  反转
        StringBuffer stringBuffer = new StringBuffer("abcdef");
        stringBuffer.reverse();
    
        // 5.capacity  容量

    1.7.18.3 StringBuilder

        // StringBuffer 和 StringBuilder 的区别
        // 代码几乎一致
        // StringBuffer是线程安全的 牺牲了性能
        // StringBuilder是线程不安全 有较高的性能
    
        // + 号连接符号的底层实现原理
            long start = System.currentTimeMillis();
            String a = "" ;
            for (int i = 0; i < 100000; i++) {
                a+="a";
            }
            long end =System.currentTimeMillis();
            System.out.println("+号连接符所耗费的时间:"+(end - start));
    
            long start1 = System.currentTimeMillis();
            StringBuffer stringBuffer = new StringBuffer() ;
            for (int i = 0; i < 100000; i++) {
                stringBuffer.append("a");
            }
            long end1 =System.currentTimeMillis();
            System.out.println("append方式连接字符串所耗费的时间:"+(end1 - start1));

    ​ // 造成性能差异的主要原因,在于+号连接符的底层原理
    ​ // String自己是不具备字符串拼接的能力
    ​ // +号先把String转换成StringBuilder 调用StringBuilder的append方法
    ​ // 再把StringBuilder转换成String

    1.7.18.4 Math数学类

    提供基本和数学相关的属性和计算的方法

    1.7.18.5 Rondom随机类

    1.7.18.6 输入流

    做输入控制

    同一个Scanner对象,最好不要间隔获取数字和字符串

    1.7.18.7日期类

        // SimpleDateFormat 日期格式转换,字符串转日期,日期转字符串
        // Calender 日历类 负责对日期时间做设置和增删
        Date date = new Date();
        System.out.println(date);
        System.out.println(date.getDay());
        System.out.println(date.getHours());
        System.out.println(date.getMonth() + 1);
        // 横线方法表示该方法已经被弃用,有可能在后续jdk版本中就不可以使用
    
        // 实例化一个日期格式化对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        // format  parse
        // format 负责将日期转换成字符串类型
        Date date = new Date();
        System.out.println(sdf.format(date));
    
        // parse 负责将字符串类型转换成日期类型
        String dateStr = "2021-07-08 16:18:46 540";
        Date date = sdf.parse(dateStr);
        System.out.println(date);

    1.7.18.8 Calender日历类

    单例类

    1.7.18.8.1 Calender与Date的转换
    //        Calender与Date进行转换
    //        采用单例模式获取日历对象
            Calendar calendar = Calendar.getInstance();
    //        通过日历对象获取日期对象
            Date date = calendar.getTime();
    //        Date()的参数表示 定义事件 距离时间原点所过去的ms数
            Date date2 = new Date(0);
    //        把这个日历 调成日期:1970.01.01  08:00:00
            calendar.setTime(date2);
    //获取日历类对象
            Calendar c = Calendar.getInstance();
            //根据日期类对象 获取当前Date对象
            Date now = c.getTime();
            // 当前日期
            //  \t tab制表符(大空格) \r\n 换行回车
            System.out.println("当前日期:\t" + format(c.getTime()));
            // 将日历的时间 定位在今天
            c.setTime(now);
    //         在当前设置时间的基础上 往后翻一个月
            c.add(Calendar.MONTH, 1);
            System.out.println("下个月的今天:\t" + format(c.getTime()));
            // 重置时间到当前时间
            c.setTime(now);
            // 修改日历的时间 单位为年
            c.add(Calendar.YEAR, -1);
            System.out.println("去年的今天:\t" + format(c.getTime()));
            // 上个月的第三天
            c.setTime(now);
            c.add(Calendar.MONTH, -1);
            c.set(Calendar.DATE, 3);
            System.out.println("上个月的第三天:\t" + format(c.getTime()));

    1.7.19 封装类

    对应八种基本数据类型的

    // 1.byte Byte
    // 2.short Short
    // 3.long Long
    // 4.int Integer
    // 5.float Float
    // 6.double Double
    // 7.boolean Boolean
    // 8.char Character

    1.7.19.1 装箱

    把基本数据类型直接转换为封装类型

    int a = 1;
    Integer i = a;
    // a,i 打印结果一致

    1.7.19.2 拆箱

    把封装类型直接转换为基本数据类型
    a,i在==比较过程中 会隐式的将封装类转换成基本数据类型
    四种整型封装类和String都支持常量池技术
    只有范围在-128到127之间的数才支持常量池技术,如果超出了这个范围,封装类的对象会直接存储在堆内存中

    int i1 = 1;
    int i2 = 2;
    int i3 = 3;
    Integer a = 3;
    Integer b = new Integer(i1) + new Integer(i2);
    Integer c = new Integer(3);
    System.out.println(a == b);//true
    System.out.println(a == c);//false
    System.out.println(b == c);//false
    System.out.println(i3==c); //true

    PS:// 没有返回值的方法,return;直接结束方法,有返回值的方法不支持

    1.8 异常

    1.8.1 异常的定义

    异常是指在程序的运行过程中所发生的不正常事件,它会中断正在运行的程序

    异常不同于程序的编译错误和逻辑错误,在程序运行时发生的错误

    1.8.2 异常的分类

    image-20210618201616755

    1.8.3 异常处理

    在上图中,对于Checked一类的异常,系统要求必须显示做出处理

    比如,我们像读取一个文件里的内容,首先要给出文件的路径,但是系统在读取文件内容时,根据这个文件路径,很有可能会找不到该文件,这种情况出现的概率是相当大的,所以必须要显示地做出处理

    在java中,异常处理是通过5个关键字来实现的,try、catch、finally、throw、throws

    try、catch、finally负责对异常的捕获处理

    throw用于手动抛出异常

    throws用于声明异常,声明方法可能要抛出的各种异常

    1.8.3.1 try-catch

    异常处理的三种情况;

    1. 程序正常执行,不会发生异常

      public void method(){
            try {
                 // 1.代码段(此处不会产生异常)
            } catch (异常类型 ex) {
                 // 2. 对异常进行处理的代码段
            }
              // 3.代码段
         }

    我们通过try-catch来包裹可会出现异常的代码。假设上述步骤1位置的代码,不会出现异常,则程序会 跳过catch代码段的部分,继续执行步骤3及后续的代码。

    1. 程序在运行时,发生了异常情况--异常类型匹配

    如果程序在步骤1处发生了异常,我们没有使用try-catch进行处理,此时,系统会被强制退出,步骤3及以后的代码,是不会被执行的

    现在有了异常处理代码,异常发生后,会被catch块捕获,如果类型异常匹配,程序会进入步骤2,执行异常处理部分,然后继续执行步骤3及后续代码

    1. 程序在运行时,发生了异常情况---异常类型不匹配

    如果发生了异常,catch块捕获异常后,但是异常类型不匹配,程序会抛出新的异常,然后终止执行,步 骤2和步骤3都不会被执行。

    多重catch代码块

    语法:

    public void method(){
       try {
            // 代码段
            // 产生异常(异常类型2)
       } catch (异常类型1 ex) {
            // 对异常进行处理的代码段
       } catch (异常类型2 ex) {
            // 对异常进行处理的代码段
       } catch (异常类型3 ex) {
            // 对异常进行处理的代码段
       }
           // 代码段
    }

    当代码可能会引发多种类型的异常时,使用多重catch代码块进行处理:

    • 排列catch语句的顺序时:先子类后父类
    • 发生异常时按顺序逐个匹配
    • 只执行第一个与异常类型匹配的catch语句

    finally

    finally块出现在try-catch的尾部,主要用于最后的处理工作,如释放资源

    image-20210618203145042

    什么时候,finally不会被执行?

    强制终止程序执行,如:System.exit(1)

    1.8.3.2 throw手动抛出异常

    当我们自己在定义方法时,对于某些操作,调用者可能进行错误的操作。

    当调用者进行了错误的操作后,我们可以手动抛出这个错误,让调用者自行处理(把包袱扔回给调用者,谁让你不使用正确的姿势来使用呢?)

    public class Student{
    private String gender;
    public void setGender(String gender){
    if ("男".equals(gender) || "女".equals(gender))
    this.gender = gender;
    else {
    throw new Exception("性别必须是\"男\"或者\"女\"!");
    }
    }
    }

    1.8.3.3 throws声明异常

    如果在一个方法体中抛出了异常,如何通知调用者?

    使用throws 声明对外抛出,对外抛出时,异常类型要保持一致

    public class Student{
    private String gender;
    public void setGender(String gender) throws Exception{
    if ("男".equals(gender) || "女".equals(gender))
    this.gender = gender;
    else {
    throw new Exception("性别必须是\"男\"或者\"女\"!");
    }
    }
    

    如果调用者调用了一个手动抛出异常的方法,该如何处理?

    1. 不处理,当然不行,编译器将无法通过编译,必须要处理。
    2. 调用者使用try-catch处理

      public static void main(String[] args) {
      Student stu = new Student();
      try {
      stu.setGender("男");
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
    3. 调用者自己不处理,继续向上抛出

      // throws Exception 抛出第4行代码的异常(我擦,烫手山芋啊,赶快抛掉)
      public static void main(String[] args) throws Exception {
      Student stu = new Student();
      stu.setGender("男");
      }

    1.9 单例模式

    单个实例,使用单例模式的类从始至终外部只能获取同一个对象

    1. 私有静态引用
    2. 私有的构造方法
    3. 对外提供唯一的获取对象的方法

    1.9.1 懒汉模式

    如果程序员不去调用getInstance()方法,对象就不会实例化, 时间换空间

    * @desc 单例模式--懒汉模式
     */
    public class SimpleTon {
    //    私有静态引用
     private static SimpleTon simpleton = null;
    //    私有的构造方法
     private SimpleTon() {
     }
    //    对外提供唯一的获取对象的方法
     public static SimpleTon getInstance() {
         if (simpleton == null) {
             simpleton = new SimpleTon();
         }
         return simpleton;
     }
    }
    
    //        调用getInstance()方法实例化对象
            SimpleTon simpleTon1 = SimpleTon.getInstance();
            SimpleTon simpleTon2 = SimpleTon.getInstance();
            System.out.println(simpleTon1 == simpleTon2);
    //  结果为true   表示调用的为同一个对象

    1.9.2 饿汉模式

    在类被加载的时候,类就会被创建

    ​ 程序员直接通过调用getInstance()方法,获取引用即可

    ​ 空间换时间

     * @desc 单例模式--饿汉模式
     */
    public class SingleTom {
    //    私有构造方法
        private SingleTom(){
            
        }
    //    私有的 静态的 当前类的引用
    //    当Singtom被加载的时候,所有的静态属性都会被加载
    //    新建的SingTom对象的地址,也会在这个时候被singTom所保存
        private static SingleTom singleTom = new SingleTom();
        
    //    对外提供唯一获取对象方法
        public SingleTom getInstance(){
            return singleTom;
        }
    }
    //        调用getInstance()方法实例化对象
    SingleTom singleTom = SingleTom.getInstance();
    SingleTom singleTom1 = SingleTom.getInstance();
    System.out.println(singleTom == singleTom1);
    //  结果为true   表示调用的为同一个对象

    1.10 集合框架

    1.10.1 集合和普通数组的区别

    数组的长度是固定的,集合的长度是可变的

    集合存储的都是对象,而且对象的类型可以不一致,在开发中一般当对象多的时候,我们使用集合进行存储

    集合分为两大类,一个单列集合java.util.Collection集合,一个双列集合java.util.Map

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

    Collection

    1.10.2 ArrayList

    1.10.2.1 ArrayList常用方法

    1.10.2.1.1 add增加

    理论上可以存储任何类型的数据

        xxxxxxxxxx ArrayList list = new ArrayList();        list.add("字符串");        list.add(new Boss());java
    1.10.2.1.2 contains 判断是否存在
    System.out.println(list.contains("字符串"));//true
    1.10.2.1.3 get 获取指定位置的对象
    System.out.println(list.get(0));//字符串
    1.10.2.1.4 indexOf 获取对象所处的位置
    System.out.println(list.indexOf("字符串"));//0
    1.10.2.1.5 remove 删除
    list.remove(true);
            System.out.println(list.contains(true));//false
    1.10.2.1.6 set 替换
    list.set(0,"替换后的数值");
    System.out.println(list.contains("字符串"));//false
    1.10.2.1.7 size 获取大小
    list.size();
    1.10.2.1.8 toArray 转换为数组
    list.toArray();
    1.10.2.1.9 clear 清空
    list.clear();
    System.out.println(list.size());

    1.10.3 集合泛型

    泛型是jdk1.5新出现的特性

    泛型是用来限制集合中存放的对象所属类型的 规定了泛型之后 集合中只可以存储泛型指定的对象或者其子类对象 约定俗成,所有的集合在使用的时候都需要规定泛型

    List<Preson> presons = new ArrayList();
            presons.add(new Student());
            presons.add(new Jerk());
    //        使用强转
            Student student = (Student) presons.get(0);

    泛型类型的拓展

    1.增加公共父类接口,将泛型定义为接口类型

    2.将想存储的数据 进行封装 将封装的类定义为泛型类型

    1.10.4 集合遍历

    1.10.4.1 for循环遍历

    for循环进行遍历

    List<Boss> bossses = new ArrayList<Boss>();
    
        // 放5个Boss进入容器
        for (int i = 0; i < 5; i++) {
            bosses.add(new Boss("Boss name " + i));
        }
    
        // 第一种遍历 for循环
        System.out.println("--------for 循环-------");
        for (int i = 0; i < bosses.size(); i++) {
            Boss boss = bossess.get(i);
            System.out.println(boss);
        }
    }

    1.10.4.2 迭代器遍历

            List<Boss> bosses = new ArrayList<>();
    //        使用迭代器进行遍历
    //        根据集合bosses生成一个对应的迭代器对象
    //        迭代器读取了bosses的对象
            Iterator<Boss> iterator = bosses.iterator();
            //从最开始的位置判断"下一个"位置是否有数据
            //如果有就通过next取出来,并且把指针向下移动
            //直到"下一个"位置没有数据
            while (iterator.hasNext()) {
                Boss boss = iterator.next();
                System.out.println(boss);
            }
    
    
    //     迭代器的for写法
    System.out.println("--------使用for的iterator-------");
        for (Iterator<Boss> iterator = Bosses.iterator(); iterator.hasNext();) {
            Boss Boss = (Boss) iterator.next();
            System.out.println(Boss);
        }

    1.10.4.3 for增强遍历

    增强for循环的底层实现是 迭代器

    for(Boss boss:bosses){
        System.out.in(boss);
    }
    //使用增强型for循环可以非常方便的遍历ArrayList中的元素,这是很多开发人员的首选。
    
    //不过增强型for循环也有不足: 无法用来进行ArrayList的初始化 也无法获取当前遍历元素的下标 不过胜在方便(偷懒)

    1.10.5 LinkedList

    LinkedList也实现了List接口 底层结构是链表 但是同样也实现了Queue接口,以及Deque接口(双向链表)

    是基于双向链表的的

    jdk1.7之前 使用的是双向循环链表

    ​ 双向循环链表: 首项需要存储尾项的指针

    ​ 尾项需要存储首项的指针

    jdk1.7之后 使用的是双向链表

    ​ 双向链表(Deque):首项的前项指针为null

    ​ 尾项的前项指针为null

    ​ 可以很方便的在头尾插入删除数据 链表结构

    1.10.5.1 LinkedList常用方法

    1.10.5.1.1 addLast

    往List尾端添加元素

    LinkedList<Object> linkedList = new LinkedList<>();
    linkedList.addLast("1");
    linkedList.addLast("2");
    linkedList.addLast("3");
    System.out.println(linkedList);
    1.10.5.1.2 addFirst

    往List首端添加元素

    linkedList.addFirst("4");
    linkedList.addFirst("5");
    linkedList.addFirst("6");
    1.10.5.1.3 getFirst

    获取List首项元素

    System.out.println(linkedList.getFirst());
    1.10.5.1.4 getLast

    获取List末项元素

    System.out.println(linkedList.getLast());
    1.10.5.1.5 removeFirst

    删除首项元素

    System.out.println(linkedList.removeFirst());
    1.10.5.1.6 removeLast

    删除尾项元素

    System.out.println(linkedList.removeLast());

    1.10.6 二叉树

    什么是二叉树结构

    二叉树

    public class Node {
        // 左子节点
        public Node leftNode;
        // 右子节点
        public Node rightNode;
        // 值
        public Object value;
    }

    1.10.6.1 插入数据

    假设通过二叉树对如下10个随机数进行排序 60 4 20 70 8 0 78 81 8 74

    排序的第一个步骤是把数据插入到该二叉树中 插入基本逻辑是,小、相同的放左边,大的放右边

    代码实现

    public class Node {
        // 左子节点
        public Node leftNode;
        // 右子节点
        public Node rightNode;
      
        // 值
        public Object value;
      
        // 插入 数据
        public void add(Object v) {
            // 如果当前节点没有值,就把数据放在当前节点上
            if (null == value)
                value = v;
      
            // 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
            else {
                // 新增的值,比当前值小或者相同
                 
                if ((Integer) v -((Integer)value) <= 0) {
                    if (null == leftNode)
                        leftNode = new Node();
                    leftNode.add(v);
                }
                // 新增的值,比当前值大
                else {
                    if (null == rightNode)
                        rightNode = new Node();
                    rightNode.add(v);
                }
      
            }
      
        }
      
        public static void main(String[] args) {
      
            int randoms[] = new int[] {  60,4,20,70,8,0,78,81,8,74 };
      
            Node roots = new Node();
            for (int number : randoms) {
                roots.add(number);
            }
      
        }

    按照这个插入顺序 实际上数据已经被我们以二叉树的数据结构排列好了

    1.10.6.2 二叉树遍历

    把这些已经排好序的数据,遍历成我们常用的List或者数组的形式

    二叉树的遍历分左序,中序,右序(也有说法叫做前序 中序 后序) 以根节点和其子节点为例 数据为

    左序即: 中间的数遍历后放在左边 60 4 70

    中序即: 中间的数遍历后放在中间 4 60 70

    右序即: 中间的数遍历后放在右边 4 70 60

    1.10.6.2.1 中序遍历

    二叉树中序遍历

       // 中序遍历所有的节点
          public List<Object> values() {
            List<Object> sortedValue = new ArrayList<>();
    
            // 左节点的遍历结果
            if (null != leftNode) {
                sortedValue.addAll(leftNode.values());
            }
    
            // 当前节点
            sortedValue.add(value);
    
            // 右节点的遍历结果
            if (null != rightNode) {
                sortedValue.addAll(rightNode.values());
            }
    
            return sortedValue;
        }
      
        public static void main(String[] args) {
      
            int randoms[] = new int[] {  60,4,20,70,8,0,78,81,8,74 };
      
            Node roots = new Node();
            for (int number : randoms) {
                roots.add(number);
            }
      
            System.out.println(roots.values());
      
        }
    }

    1.10.7 HashMap

    HashMap是通过键值对存放数据的

    Map<String, String> stus = new HashMap<>();
    System.out.println("ab".hashCode());
    stus.put("渣男", "2");
    stus.put("直男", "1");
    System.out.println(stus.get("直男"));

    如果在设置key value的时候 key重复了

    新key的value会覆盖旧key的value

    key - value 可以为空

    HashMap是所有已知数据结构中 查询元素最快的集合(没有之一)

    1.10.7.1 hashCode

    HashMap是所有已知数据结构中 查询元素最快的集合(没有之一)

    底层hashCode

    如果两个对象相等 那么hashCode值一定相等

    如果两个对象的hashCode值相等,并且通过equals进行比较之后

    equals结果也相等 这个时候才可以说两个对象相等

    原生的hashmap key需要hashcode方法 equals方法
    是由key所属类提供的
    尤其注意 如果是自定义类作为hashMap的key 一定要 重写equals方法

    hashCode方法 一般来说是由程序员自己定义的
    hashCode hash冲突越多 链表的长度就越长 查询的性能就越低
    hash冲突越少

    自己实现HashMap

    public class Entry {
        public String key;
        public Object value;
    
        public Entry(String key, Object value) {
            this.key = key;
            this.value = value;
        }
    }
    public class DiyHashMap {
        public DiyHashMap(){
            for (int i = 0; i < 2000; i++) {
                list.add(i,null);
            }
        }
        //    定义一个数组 用来匹配的是原生HashMap中的数组
    //数组的初始化长度为2000
        public static ArrayList<LinkedList<Entry>> list =
                new ArrayList();
        //    diy一个HashCode方法 使每一个HashCode值 对应一个ArrayList的下标
        public int hashCode(String key){
            int hashCode = key.hashCode()%2000;
            return Math.abs(hashCode);
        }
    
        public Object get(String key){
            Object value = null;
    //             先获取key所对应的hashcode
            int hashCode = hashCode(key);
            LinkedList<Entry> entryList = list.get(hashCode);
    //        如果这个链表为空 说明该hashcode下 没有存过任何值
            if(entryList==null){
                return value;
            }else{
                for (Entry entry:entryList) {
                    //如果在遍历链表的时候 找到了匹配的key 获取这个entry
                    // 并把该entry对象的value取出 用于一会的返回
                    if(entry.key.equals(key)) {
                        value = entry.value;
                        break;
                    }
                }
                return value;
            }
        }
    
        public void put(String key,Object value){
            //先把键值对封装在Entry对象中
            Entry entry = new Entry(key,value);
            //根据提供的HashCode方法 把Entry对象放置在对应的链表结构中
            int hashCode = hashCode(key);
            //获取链表的引用
            LinkedList<Entry> entryList = list.get(hashCode);
            //先判断指定的数组下标位置的链表是否为null
            if(entryList==null){
    //            如果没有链表,说明这个hashcode所对应的数组元素位置
                // 还没有存放过任何元素
                entryList = new LinkedList<Entry>();
                list.set(hashCode,entryList);
                entryList.add(entry);
            }else{
                // 提供一个布尔值 用来判断有没有找到重复的key
                boolean found = false;
    
                //当前数组下标位置中 有对应的链表
                // 先判断存不存在key相同的Entry
    
                loop:for (int i = 0; i < entryList.size(); i++) {
                    //新插入的键值对 如果key和链表中原有的key相等
                    // 则让新key的value 替换旧key的value
                    if(entry.key.equals(entryList.get(i).key)){
                        entryList.get(i).value = entry.value;
                        found = true;
                        break loop;
                    }
    
                }
                if(!found){
                    //没找到重复的元素 则需要将当前的Entry添加到链表中
                    entryList.add(entry);
                }
            }
        }

    1.10.7.2 HashMap遍历

            HashMap<String, Object> hashMap = new HashMap();
            hashMap.put("1", 1);
            hashMap.put("2", 2);
            hashMap.put("3", 3);
    1.10.7.2.1 迭代器遍历
            Iterator<Map.Entry<String, Object>> iterator = hashMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, Object> e = iterator.next();
                System.out.println(e.getKey() + " " + e.getValue());
            }
    1.10.7.2.2 增强for循环遍历
    Set<String> hSet = hashMap.keySet();
            for (String s:hSet) {
                System.out.println(s+" "+hashMap.get(s));
            }

    1.10.8 HashTable

    HashMap 和 Hashtable 几乎一致

    但是Hashtable 是线程安全

    HashMap是线程不安全的

    Hashtable key和value皆不能为null

    1.10.9 Set

    Set数据不重复

    并且只能能通过iterator迭代器遍历

    1.10.10 HashSet

    HashSet的数据不保证有序

    本质上是一个HashMap

    只不过他的所有value都是类中所定义的一个静态final对象 Object

    HashSet就是HashMap的key

    HashSet<Integer> hs  = new HashSet<>();
    hs.add(1);
    hs.add(2);
    hs.add(3);
    hs.add(4);
    hs.add(5);
    hs.add(6);
    hs.add(7);
    hs.add(8);
    System.out.println(hs);
    LinkedHashSet<Integer> lhs = new LinkedHashSet<>();
    //        数据排列顺序与插入顺序一致

    第二次插入同样的数据,是插不进去的,容器中只会保留一个

    1.10.10.1 HashSet遍历

    Set不提供get()来获取指定位置的元素 所以遍历需要用到迭代器,或者增强型for循环

    HashSet<Integer> numbers = new HashSet<Integer>();
             
            for (int i = 0; i < 20; i++) {
                numbers.add(i);
            }
    1.10.10.1.1 迭代器遍历
      for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
                Integer i = (Integer) iterator.next();
                System.out.println(i);
            }
    1.10.10.1.2 增加for循环遍历
    for (Integer i : numbers) {
                System.out.println(i);
            }

    1.11 集合接口Collection

    Collection是 Set List Queue和 Deque的接口

    Queue: 先进先出队列

    Deque: 双向链表

    Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的

    Deque 继承 Queue,间接的继承了 Collection

    集合接口

    1.12 集合工具类

    public class Student {
    
        private static int initId = 0;
        private int id;
        private String name;
        private int height;
        private int weight;
    }
    

    1.12.1 reverse 反转

    ArrayList<Student> al = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        al.add(Student.getRandomStudent());
    }
    Collections.reverse(al);
    System.out.println(al);

    1.12 2 shuffle 混淆

    Collections.reverse(al);

    1.12.3 sort 排序

    Collections.sort(al);

    当list中存放的是自定义的类时,比较方法有两种

    1.12.3.1 实现Compareable接口

        @Override
        public int compareTo(Student anotherStudent) {
    //        如果>的判断 返回值为1  那么数据则按照你指定的
            // 属性 进行升序排序
            if(height<anotherStudent.height){
                return 1;
            }
            else{
                return -1;
            }
        }

    1.12.3.2 使用额外接口定义比较规则

    Comparator<Student> comparator = new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
            if(s1.getHeight()>s2.getHeight()){
                return 1;
            }else
                return -1;
        }
    };

    1.12.4 swap 交换

    Collections.swap(al,0,2);

    1.12.5 rotate 滚动

    Collections.rotate(2);

    1.12.6 synchronizedList 线程安全化

    1.13 MVC

    model 模型层 实体类以及数据源

    ​ view 视图层 sout 和 Scanner 组成的界面

    ​ Controller 控制器层 决定了根据用户输入的数据访问不同的功能

    ​ Service 业务层 你的逻辑 和主要代码实现 都应该定义在业务层

    1.14 文件类对象和IO流

    1.14.1 文件类对象

    文件类对象是java中专门用来操作数据的对象。

        public static void main(String[] args) {
    //        新建一个文件对象 并且定义了文件对象所对应的目录
            File file1 = new File("d:/test");
            System.out.println(file1.getAbsolutePath());
    
    //        基于相对路径进行创建  基于当前的项目所在目录
            File file2 = new File("jerry.txt");
            System.out.println(file2.getAbsolutePath());
    
    //        以一个文件对象 作为所创建文件对象的父目录
            File file3 = new File(file1,"Bob.txt");
            System.out.println(file3.getAbsolutePath());
    
    //        需要通过文件类所提供的其它方法  完成更多操作
        }

    1.14.2 文件类对象常用方法

        File f = new File("d:/zhanan/xdx.txt");
        System.out.println("当前文件是:" +f);

    1.14.2.1 exists

        //文件是否存在
        System.out.println("判断是否存在:"+f.exists());

    1.14.2.2 isDirectory

        //是否是文件夹
        System.out.println("判断是否是文件夹:"+f.isDirectory());

    1.14.2.3 isFile

        //是否是文件(非文件夹)
        System.out.println("判断是否是文件:"+f.isFile());

    1.14.2.4 length

        //文件长度
        System.out.println("获取文件的长度:"+f.length());

    1.14.2.5 lastModified

     //文件最后修改时间
        long time = f.lastModified();
        Date d = new Date(time);
        System.out.println("获取文件的最后修改时间:"+d);
        //设置文件修改时间为1970.1.1 08:00:00
        f.setLastModified(0);

    1.14.2.6 renameTo

    //文件重命名
        File f2 =new File("d:/zhanan/hanzheng.txt");
        f.renameTo(f2);
        System.out.println("修改成功");

    1.14.2.7 list

    // 以字符串数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
        f.list();

    1.14.2.8 listFiles

    // 以文件数组的形式,返回当前文件夹下的所有文件(不包含子文件及子文件夹)
        File[]fs= f.listFiles();

    1.14.2.9 getParent

    // 以字符串形式返回获取所在文件夹
        f.getParent();

    1.14.2.10 getParentFile

    // 以文件形式返回获取所在文件夹
        f.getParentFile();

    1.14.2.11 mkdir

        // 创建文件夹,如果父文件夹skin不存在,创建就无效
        f.mkdir();

    1.14.2.12 mkdirs

        // 创建文件夹,如果父文件夹skin不存在,就会创建父文件夹
        f.mkdirs();

    1.14.2.13 createNewFile

        // 创建一个空文件,如果父文件夹skin不存在,就会抛出异常
        f.createNewFile();
        // 所以创建一个空文件之前,通常都会创建父目录
        f.getParentFile().mkdirs();

    1.14.2.14 listRoots

        // 列出所有的盘符c: d: e: 等等
        f.listRoots();

    1.14.2.15 delete

        // 刪除文件
        f.delete();

    1.14.2.16 deleteOnExit

        // JVM结束的时候,刪除文件,常用于临时文件的删除
        f.deleteOnExit(); 
    //找出当前文件夹中最小最大的文件
    Scanner scanner = new Scanner(System.in);
    String path = scanner.nextLine();
    File file = new File(path);
    
    //用数组的方式
    File[] fs = file.listFiles();
    File fileMax = fs[0];
    File fileMin = fs[0];
    for (int i = 0; i < fs.length; i++) {
        if (fs[i].length() > fileMax.length()) {
            fileMax = fs[i];
        } else if (fs[i].length() <= fileMin.length() && fs[i].length() != 0) {
            fileMin = fs[i];
        }
    }
    System.out.println("最大文件"+fileMax.getAbsolutePath() + "  " + fileMax.length());
    System.out.println("最小文件"+fileMin.getAbsolutePath() + "  " + fileMin.length());
    
    //使用集合集合进行查找
    ArrayList<File> fileList = new ArrayList();
            File fileArray[] = file.listFiles();
    //        把文件类对象 经过筛选后 放入到集合fileList中
            for (File f : fileArray) {
                if (f.length() != 0 && f.isFile()) {
                    fileList.add(f);
                }
            }
            Comparator<File> comparator = new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (o1.length() > o2.length()) {
                        return 1;
                    } else if (o1.length() == o2.length()) {
                        return 0;
                    } else {
                        return -1;
                    }
                }
            };
    
            Collections.sort(fileList, comparator);
            System.out.println(fileList.get(0).getAbsolutePath() + " " + fileList.get(0).length());
            System.out.println(fileList.get(fileList.size() - 1).getAbsolutePath() + " " + fileList.get(fileList.size() - 1).length());
    

    1.14.3 流

    流可以理解为数据传输的通道,介质,媒介

    1.14.3.1 字节流

    InputStream 字节输入流

    OutputStream 字节输出流

    本身是本身只是抽象类 只负责定义流需要做的事情 不提供具体的方法实现

    主要依靠FileInputStream和FileOutputStream

    字节流不仅可以读取文本

    二进制文本txt

    二进制非文本 jpg avi rbmv

    字节流是可以读取中文的 但是需要进行大量的编码设置和切换

    为了简化这个流程 可以使用字符流

    1.14.3.1.1 FileInputStream 字节输入流
    try {
                //准备文件其中的内容是AB,对应的ASCII分别是65 66
                File f = new File("E:/student/seaKing/poolman.txt");
                //创建基于文件创建的字节输入流
                FileInputStream fis = new FileInputStream(f);
                //创建字节数组,其长度就是文件的长度
    //            用来后续接收从文本中读到的数据
    //            数据数文本中 每一个字符所对应的字符集编码
                byte[] all = new byte[(int) f.length()];
                //以字节流的形式读取文件所有内容
    //            存储在本地定义的紫萼数据all中
                fis.read(all);
                for (byte b : all) {
                    //打印出来是65 66
                    System.out.println(b);
                }
    
                //每次使用完流,都应该进行关闭
    //            减少资源开销
                fis.close();
    
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    1.14.3.1.2 FileOutputStream 字节输出流
    try {
        // 准备文件lol2.txt其中的内容是空的
        File f = new File("E:/student/seaKing/poolman.txt");
        // 准备长度是2的字节数组,用88,89初始化,其对应的字符分别是X,Y
        byte data[] = { 88, 89 };
    
        // 创建基于文件的输出流
        FileOutputStream fos = new FileOutputStream(f);
        // 把数据写入到输出流
        fos.write(data);
        // 关闭输出流
        fos.close();
    
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    1.14.3.2 流的三种关闭方式

    1. fileInPutStream.close()
    2. //        try with 块  在try后面添加()括号内定义的所有流都会在使用完之后自动关闭
      //        java中新增了一个aotoCloseable接口
      //        所有实现该接口的类 都可以通过try()来实现流的自动关闭
              File f = new File("E:/student/seaKing/poolman.txt");
              try(FileOutputStream fileOutputStream = new FileOutputStream(f)){
                  
              }catch (Exception e){
                  e.printStackTrace();
              }
    3. try{
          
      }catch(Exception e){
          e.printStackTrace();
      }finally{
          try{
              fileInputStream.close;
          }catch(Exception e){
              e.printStackTrace;
          }
      }

    1.14.3.3 字符流

    1.14.3.3.1 FileReader字符输入流
    //        字符输入流
            // 准备文件abc.txt其中的内容是AB
            File f = new File("e:/abc.txt");
            // 创建基于文件的Reader
            try (FileReader fr = new FileReader(f)) {
                if (!f.exists()) {
                    f.createNewFile();
                }
                // 创建字符数组,其长度就是文件的长度
                char[] all = new char[(int) f.length()/3];
                // 以字符流的形式读取文件所有内容
                fr.read(all);
                for (char b : all) {
                    // 打印出来是A B
                    System.out.print(b);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    1.14.3.3.2 FileWriter字符输出流
    File f = new File("E:/abc.txt");
    
    try(FileWriter fw = new FileWriter(f)){
        String data = "小兔子乖乖拔萝卜";
        char[] dataChar = data.toCharArray();
        fw.write(dataChar);
    }catch (Exception e){
        e.printStackTrace();
    }

    1.14.3.4 缓存流

    以介质是硬盘为例,字节流和字符流的弊端: 在每一次读写的时候,都会访问硬盘。 如果读写的频率比较高的时候,其性能表现不佳。

    为了解决以上弊端,采用缓存流。

    缓存流在读取的时候,会一次性读较多的数据到缓存中,以后每一次的读取,都是在缓存中访问,直到缓存中的数据读取完毕,再到硬盘中读取。

    缓存流在写入数据的时候,会先把数据写入到缓存区,直到缓存区达到一定的量,才把这些数据,一起写入到硬盘中去。按照这种操作模式,就不会像字节流,字符流那样每写一个字节都访问硬盘,从而减少了IO操作

    1.14.3.4.1 BufferReader缓存流读取数据

    缓存流是高级流 必须基于基础流中的字符流 才能创建
    数据在缓存流建立的时候 就已经被加载到缓存中
    我们要做的事情 将数据从缓存中提取到jvm本地

    File file = new File("e:/abc.txt");
    try(
            FileReader fr = new FileReader(file);
            BufferedReader br = new BufferedReader(fr);
            ){
        while (true){
            // 一次读一行
            String line = br.readLine();
            if (null == line) {
                break;
            }
            System.out.println(line);
        }
    }catch(Exception e){
        e.printStackTrace();
    }
    1.14.3.4.2 PrintWriter缓存流写入数据
    // 向文件写入数据
    
            File f = new File("e:/abcd.txt");
    
            try (
                    // 创建文件字符流
    
    
                    // 数据以追加方式写入文件,而不是覆盖
    
                    FileWriter fw = new FileWriter(f,true);
                    //默认情况下 缓存流是会覆盖文本原有所有数据的 所以需要
                    //在原文本追加数据的情况下
                    //需要这样写
                    //PrintWriter fw = new PrintWriter(f,true);
                    // 缓存流必须建立在一个存在的流的基础上
    
    //                缓存流基于字符流去写入数据的时候 会自动进行拼接 而不死覆盖
    //                PrintWriter pw = new PrintWriter(fw);
    
    //                缓存会在数据写入的时候  自动进行提交
                    PrintWriter pw = new PrintWriter(fw,true);
    
    
            ) {
    //            所有操作都是对缓存进行的 缓存会选择合适的时机  进行自动提交
                pw.println("你爱我呀我爱你");
                // 如果需要立刻提交缓存中的数据 可以使用
                // pw.flush()进行缓存提交
    //            flush保证因为异常情况出现的导致数据无法写入
    
                pw.println("蜜雪冰城甜蜜蜜");
                pw.println("喂!三點幾了喂。做做做做撚啊做。飲茶先啊");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

    1.14.3.5 数据流

    数据流用于一些需要进行标准格式化输入输出的场景下,进行指定数据类型进行数据传递的场景下,后续会在socket套接字场景下使用到,现阶段仅做使用了解

    DataInputStream 数据输入流 DataOutputStream 数据输出流

    使用数据流的writeUTF()和readUTF() 可以进行数据的格式化顺序读写

    要用DataInputStream 读取一个文件,这个文件必须是由DataOutputStream 写出的,否则会出现EOFException,因为DataOutputStream 在写出的时候会做一些特殊标记,只有DataInputStream 才能成功的读取。

    public static void main(String[] args) {
        write();
        read();
    }
    
    private static void read() {
        File f =new File("d:/abaaba.txt");
        try (
                FileInputStream fis  = new FileInputStream(f);
                DataInputStream dis =new DataInputStream(fis);
        ){
            boolean b= dis.readBoolean();
            int i = dis.readInt();
            String str = dis.readUTF();
             
            System.out.println("读取到布尔值:"+b);
            System.out.println("读取到整数:"+i);
            System.out.println("读取到字符串:"+str);
    
        } catch (IOException e) {
            e.printStackTrace();
        }
         
    }
    
    private static void write() {
        File f =new File("d:/abaaba.txt");
        try (
                FileOutputStream fos  = new FileOutputStream(f);
                DataOutputStream dos =new DataOutputStream(fos);
        ){
            dos.writeBoolean(true);
            dos.writeInt(300);
            dos.writeUTF("两只老虎爱跳舞");
        } catch (IOException e) {
            e.printStackTrace();
        }
         
    }

    1.14.3.6 对象流

    对象流指的是可以直接把一个对象以流的形式传输给其他的介质,比如硬盘

    一个对象以流的形式进行传输,叫做序列化

    该对象所对应的类,必须是实现Serializable接口 序列化在前后端交互的步骤中用的很多

    比如把本地java对象或者java集合 转换成JSON字符串或者是JS数组对象格式的字符串

    public class Student implements Serializable {
    //表示这个类当前的版本,如果有了变化,比如新设计了属性,就应该修改这个版本号
    private static final long serialVersionUID = 1L;
    public String name;
    public int age;
    
    }

    测试代码

    public static void main(String[] args) {
        //创建一个 Student 
        //要把 Student对象直接保存在文件上,务必让Student类实现Serializable接口
        Student s = new Student();
       s.name = "xdx";
        s.age = 666;
          
        //准备一个文件用于保存该对象
        File f =new File("d:/xdx.txt");
    
        try(
            //创建对象输出流
            FileOutputStream fos = new FileOutputStream(f);
            ObjectOutputStream oos =new ObjectOutputStream(fos);
            //创建对象输入流              
            FileInputStream fis = new FileInputStream(f);
            ObjectInputStream ois =new ObjectInputStream(fis);
        ) {
            oos.writeObject(h);
            Student s2 = (Student) ois.readObject();
            System.out.println(s2.name);
            System.out.println(s2.age);
               
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
            
    }

    1.14.3.7 通过url实现文件下载

    url下载

    commons 完成上传

    net 提供了对应的类 去实现下载功能

    1.需要提供本地对象 以及对应的文件对象 用来接收下载的内容

    2.需要提供下载源

    3.所有的数据写入 要借助字节流去完成

    4.不要对下载速度有任何的期待

    public class UrlTest {
        public static void main(String[] args) {
            //http请求连接 专门负责和http协议的网站进行数据通信使用
            HttpURLConnection connection = null;
            // 定义的流的引用 用来接收后续返回的对象的
            InputStream is = null;
            //字节输出流
            FileOutputStream fos = null;
    
            try  {
                //统一资源定位符
                // http://www.baidu.com
                URL url = new URL
                        ("https://dldir1.qq.com/qqfile/qq/PCTIM/TIM3.3.8/TIM3.3.8.22043.exe");
    
                //获取到HttpURLConnction对象
                connection = (HttpURLConnection) url.openConnection();
    
                //通过连接对象获取到输入流
                is = connection.getInputStream();
    
                //文件保存位置
                fos = new FileOutputStream(new File("d:/TIM3.3.8.22043.exe"));
    
                //定义字节数组 用来 接收 从目标url获取到的数据
                byte[] buffer = new byte[1024];
                int len;
                //只要数据不读完 就一直通过输入流 进行数据读取
                while ((len = is.read(buffer)) != -1) {
                    // 通过我们定义的字节输出流 以1024byte作为写入数据的单元
                    // 进行数据写入
    
                    fos.write(buffer, 0, len);
    
                }
                System.out.println("下载完毕");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    2 JDBC

    java database connection

    传统方法

    1. 数据不能持久化
    2. 操作繁琐
    3. 访问静态资源非常耗费性能

    用数据库代替本地集合

    借助JDBC

    2.1 安装配置mysql

    2.1.1 Linux环境安装

    https://www.cnblogs.com/z0909y/p/10772854.html

    2.1.2 云服务器

    2.2 mysql连接jdbc步骤

    mysql的所有的包都是向下兼容

    1. 项目导入包
    2. 加载驱动

      Class.forName("com.mysql.cj.jdbc.Driver");

    mysql8.0之前版本需要去掉cj

    1. 创建连接

    提供要连接的数据库的基本信息
    mysql ip地址 端口号 database 以及相关参数

    Connection c ;
            try {
                // 声明的连接引用
                c = DriverManager.getConnection("jdbc:mysql://101.34.70.96:3306/jerryDb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
                        "admin", "123456");
            } catch (Exception e){
                e.printStackTrace();
            }

    SSL: 安全认证协议

    ​ 用户在带有该协议的网站输入任何数据都是安全的

    ​ 不带s(http) 数据安全存在隐患

    创建编译语句

    // 创建编译语句 Statement PrepareStatement
    String sql = "insert into Student values(null,"+"'欣欣'"+","+"18"+")";
    Statement s = null;
    try {
         s = c.createStatement();
    } catch (SQLException e) {
        e.printStackTrace();
    }
    System.out.println(sql);

    每一个SQL语句 对应一个statement

    1. 执行sql语句

      // 执行sql语句
      s.execute(sql);
    2. 关闭连接

      // 关闭连接
      c.close();

    statement会在connection关闭时自动关闭

    2.3 jdbc增删改

    增删改都不需要数据库返回任何值 替换sql语句即可

    2.4 jdbc查询

    public static void list(int age) {
            Connection c = getConnect();
            Statement s = null;
            try {
                s = c.createStatement();
                String sql = String.format("select * from Student where sAge=%d", age);
    
    //            查询语句在sql语句执行后  会返回一个结果集
    //            ResultSet 专门用来存放数据库通过jdbc返回的java数据
    //            查询语句 使用executeQuery方法运行,而不是execute
                ResultSet rs = s.executeQuery(sql);
    //            从结果集中 将想要的数据取出
    //            把结果集  想象成迭代器
    //            next()方法 返回值为boolean 代表指针所指向的下一行数据是否为空
    //            re.next()方法在第一次运行的时候,将指针只想结果集的第一行数据
                while (rs.next()) {
    //                结果集中 数据是按照字段名称和数据类型进行存储的
    //                我们在提取数据的时候需要根据字段的名称和数据类型 从rs指针所指向的当前行中提取数据
    
                    int id = rs.getInt("id");
                    String sName = rs.getString("sName");
                    int sAge = rs.getInt("sAge");
    
                    System.out.printf("学生id:%d,学生姓名:%s,学生年龄:%d", id, sName, sAge);
                    System.out.println();
    
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                c.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    //                字段名称也可以替换为 字段在查询结果中的顺序
    //                int id = rs.getInt(1);
    //                String sName = rs.getString(2);
    //                int sAge = rs.getInt(3);

    2.5 PreparedStatement

    statement的安全隐患:SQL注入 (黑客门槛最低的攻击方式)

    由于statement自身的特性所决定的

    先拼接sql语句,然后再把sql语句交给数据库编译运行

    //        使用preparedStatement执行学生表的查询和增加
        public static void list(int id) throws SQLException {
            Connection c = Test2.getConnect();
    //        使用的是PreparedStatement 需要再创建对象之前就确定sql语句
            String sql = "select * from Student where id = ?";
            PreparedStatement ps = c.prepareStatement(sql);
    //        对?的值进行参数的传递
            ps.setInt(1,id);
            ResultSet rs = ps.executeQuery();
            if (rs.next()){
                int idi = rs.getInt("id");
                String sName = rs.getString("sName");
                int sAge = rs.getInt("sAge");
                System.out.printf("id:%d,name:%s,age:%d",idi,sName,sAge);
            }
            c.close();
        }

    2.6 JDBC获取主键

    //        如果后续需要获取主键,则添加 Statement.RETURN_GENERATED_KEYS参数
            PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    ResultSet rs = ps.getGeneratedKeys();

    2.7 JDBC事务支持

    c.setAutoCommit(false); 关闭事务自动提交
    
    c.rollback(); 数据回滚
    
    c.commit(); 事务手动提交
    public static void affairCommit() {
            Connection c = null;
            try {
                c = Test2.getConnect();
    //        关闭jdbc默认的事务的自动提交 关闭之后 所有sql语句的操作
    //        都需要手动提交 才能把数据从数据库缓存中写入到库中
                c.setAutoCommit(false);
                Statement s1 = c.createStatement();
                String sql1 = "insert into Student values(null,'张浩博',20)";
                s1.execute(sql1);
                Statement s2 = c.createStatement();
    //            故意写错 模拟事务
                String sql2 = "insert into Student vlue(null,'王文达',20)";
                s2.execute(sql2);
                c.commit();
            } catch (SQLException e) {
                try {
                    c.rollback();
                    System.out.println("发生异常,数据回滚");
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
                e.printStackTrace();
            }
            try {
                c.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    2.8 ORM概念

    Object Relation Model 对象关系模型

    实体类 封装类

    用java的一个类 映射数据库的一张表

    用实体类的成员变量映射数据库的字段

    实体类的一个对象映射数据库的一行记录

    实体类的一个对象的属性值映射数据库表中某一行记录中 某一个字段的值

    把java的实体类的对象 当作数据库数据的容器

    2.9 DAO封装

    2.10 数据库连接池

    数据库连接池原理------传统方式

    当有多个线程,每个线程都需要连接数据库执行SQL语句的话,那么每个线程都会创建一个连接,并且在使用完毕后,关闭连接。

    创建连接和关闭连接的过程也是比较消耗时间的,当多线程并发的时候,系统就会变得很卡顿。

    同时,一个数据库同时支持的连接总数也是有限的,如果多线程并发量很大,那么数据库连接的总数就会被消耗光,后续线程发起的数据库连接就会失败。

    数据库连接池

    连接池原理

    与传统方式不同,连接池在使用之前,就会创建好一定数量的连接。

    如果有任何线程需要使用连接,那么就从连接池里面借用,而不是自己重新创建。

    使用完毕后,又把这个连接归还给连接池供下一次或者其他线程使用。

    倘若发生多线程并发情况,连接池里的连接被借用光了,那么其他线程就会临时等待,直到有连接被归还回来,再继续使用。

    整个过程,这些连接都不会被关闭,而是不断的被循环使用,从而节约了启动和关闭连接的时间。

    后续我们在框架使用时 会使用封装好的连接池 比如c3p0 durid 但是我们还是手动实现一遍

    连接池类

    1. ConnectionPool() 构造方法约定了这个连接池一共有多少连接
    2. 在init() 初始化方法中,创建了size条连接。 在过程中 连接不可以被关闭 必须保持活跃状态以备后续使用
    3. getConnection, 判断是否为空,如果是空的就wait等待,否则就借用一条连接出去
    4. returnConnection, 在使用完毕后,归还这个连接到连接池,并且在归还完毕后,调用notifyAll,通知那些等待的线程,有新的连接可以借用了。
    public class ConnectionPool {
        //    定义的连接容器
        List<Connection> cs = new ArrayList();
        //    提供连接池中的连接数量
        int connectionPoolSize;
    
        public ConnectionPool(int connectionPoolSize) {
            this.connectionPoolSize = connectionPoolSize;
            init();
        }
    
        //    创建size条连接  在创建的过程中连接不可以被关闭  必须保持活跃状态 已被后续使用
        public void init() {
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                for (int i = 0; i < connectionPoolSize; i++) {
                    Connection c = DriverManager.getConnection
                            ("jdbc:mysql://101.34.70.96:3306/jerryDb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
                                    "admin", "123456");
    //                把连接引用放入到连接池中
                    cs.add(c);
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        //    提供一个获取连接的方法
        public synchronized Connection getConnection() {
    //        如果在线程访问的时候  存放连接的集合为空 就让当前来获取连接的线程进入wait状态
            while (cs.isEmpty()) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Connection c = cs.remove(0);
            return c;
        }
    
        //    提供一个归还连接的方法
        public synchronized void returnConnection(Connection c) {
            cs.add(c);
            this.notifyAll();
        }
    }

    初始化一个三条连接的数据库连接池

    定义100个线程 模拟100个用户获取连接

    有借有还 每一个线程拿到连接之后 执行一个耗时1s的sql语句

    public class TestConnectionPool {
    
        static class WorkingThread extends Thread{
            private ConnectionPool cp;
    
            @Override
            public void run() {
                Connection c = cp.getConnection();
                System.out.println(this.getName()+"获取到了一个连接,并开始干活");
                    try(Statement st = c.createStatement();) {
                        Thread.sleep(1000);
                        st.execute("select * from Student");
                    } catch (SQLException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cp.returnConnection(c);
    
            }
    
            //        构造方法负责定义线程的名称和传入连接池对象的引用
            public WorkingThread(String name, ConnectionPool cp){
                super(name);
                this.cp = cp;
            }
        }
    
        public static void main(String[] args) {
            ConnectionPool cp = new ConnectionPool(3);
            for (int i = 0; i < 100; i++) {
                new WorkingThread("调用连接池的线程"+i,cp).start();
            }
        }
    }

    3 多线程

    每一个程序 是由多个进程组成的

    每一个进程 又具备多种多样的功能

    进程的每一个功能实现 都是来自于一个个不同的线程组成的

    每一个进程被运行 都需要被CPU分配金额提供对应的运算资源

    CPU在单位时间内 只能给一个进程分配运行资源

    在java中 jvm会对java中所创建的线程进行资源的分配

    3.1 实现线程常用方式

    3.1.1 继承Thread类

    每一个线程类都需要定义一个run方法

    run方法代表的是线程在活动期间所作的事情

    public class Test1 extends Thread{
     //重写run方法
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("干饭人   干饭魂!");
            }
        }
    }

    创建线程对象

    public static void main(String[] args) {
            Test1 tt1 = new Test1("干饭人张浩博");
            Test1 tt2 = new Test1("卖饭人");
            Test1 tt3 = new Test1("炒饭");
        }

    调用start()方法 会使线程进入到就绪态
    只有进入到就绪态的线程 才会被jvm选中 分配资源 得以运行
    每一个线程 都会执行自己的run方法
    jvm选择哪一个线程线程运行 运行多久 都是不可控的
    线程的执行顺序 和对象的创建顺序 以及start启动顺序 是完全无关的

            tt1.start();
            tt2.start();
            tt3.start();

    3.1.2 实现Runable接口

    当前线程引用获取的方式 有所变化

    public class Test2 implements Runnable{
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("管老师,我可以不吃饭,一直敲代码,我叫:"+Thread.currentThread().getName());
            }
        }
    }

    实现Runable接口时,对象是不能直接实例化的,还是要借助Thread类完成线程对象的实例化

    Test2 t1 = new Test2();
    Thread tt1 = new Thread(t1);
    tt1.setName("程序员章");

    3.1.3 通过匿名内部类

    (本质上与第一种没区别) 只不过写法不同

    Thread t1 = new Thread(){
        //重写run方法
        @override
        public void run(){
            
        }
    };

    3.1.4 实现callable接口 (了解即可)

    3.2 线程休眠

    sleep方法 会使当前线程 暂停运行 并且释放jvm资源

    其它线程可以在其sleep期间 争抢jvm的资源调度

    使当前正在运行的线程 进入到sleep状态

    Thread.sleep(2000);

    3.3 join

    main方法其实对应的是我们程序的主线程 主线程运行了 其他线程对应的对象才会被实例化和创建

    public class Test7 {
        public static void main(String[] args) {
            Thread t1 = new Thread(){
              @Override
              public void run(){
                  for (int i = 0; i < 10; i++) {
                      System.out.println("我在执行线程方法,我叫:"+this.getName());
                  }
              }
            };
    
            t1.setName("t1");
            t1.start();
    
            Thread t2 = new Thread(){
                @Override
                public void run(){
                    for (int i = 0; i < 10; i++) {
                        System.out.println("我在执行线程方法,我叫:"+this.getName());
                    }
                }
            };
    
            t2.setName("t2");
            t2.start();
            //代码运行到这一行的时候,所有的线程定义以及start方法调用都是由主线程完成的
            try {
                //相当于t1线程加入到主线程中 只有t1 线程运行完成之后
                //才会继续执行 下方的代码
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            Thread t3 = new Thread(){
                @Override
                public void run(){
                    for (int i = 0; i < 10; i++) {
                        System.out.println("我在执行线程方法,我叫:"+this.getName());
                    }
                }
            };
    
            t3.setName("t3");
            t3.start();
        }
    }

    3.4 线程优先级

    优先级高的线程 会有更大的几率竞争到jvm资源

    但是 高优先级并不意味着绝对 只是相对几率较高

    //        System.out.println(Thread.MAX_PRIORITY);//最高优先级为10
    //        System.out.println(Thread.MIN_PRIORITY);//最低优先级为1
    //设置线程的优先级
    t1.setPriority(10);
    t2.setPriority(1);

    3.5 线程让步

    线程让步 yield

    调用该方法的线程

    会临时将jvm资源直接让出

    再和其它所有线程重新竞争资源

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println("张浩博在干啥呢,别隔着隔着了"+this.getName());
                    Thread.yield();
                }
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("t2跑起来了"+this.getName());
                }
            }
        };
        t1.start();
        t2.start();
    }

    3.6 守护线程

    本质是线程

    会在所有其它非守护线程都运行结束之后 停止运行

    java经典守护线程:GC回收机制

    public static void main(String[] args) {
            Thread t1 = new Thread(){
                @Override
                public void run() {
                    int times = 0;
                    while(true){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("已经过去了"+ ++times+"秒");
                    }
                }
            };
            t1.setName("t1");
    
            Thread t2 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("我是非守护线程t2,当前for循环运行次数为"+i);
                    }
                }
            };
    
    //        把t1设置为守护线程
            t1.setDaemon(true);
            t1.start();
            t2.start();
    //        为了体现t1作为守护线程的特性 让主线程sleep一段时间
    //        观察守护线程t1在这个过程中做了什么事
            System.out.println("主线程开始sleep");
            try {
                Thread.sleep(200000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程即将运行结束");
        }

    3.7 线程同步

    锁

    多个线程同时操作一个数据 可能会导致数据发生偏差

    通过synchronized 实现线程同步

    3.7.1 synchronized原理

    每一个对象都有且只有一把锁

    java中 任何对象都可以被当作是同步对象

    sleep不会使当前线程释放锁 在sleep期间 当前线程依旧持有锁

    如果对静态方法添加sychronized概念关键字 线程获取的同步对象并不是不同对象 而是类对象

    在java中 每一个类在jvm中 都会有且只有一个特殊的对象叫做类对象

    ClassLoader 在加载类之前 会获取类的类对象

    类对象数据属于高级部分中 反射

    3.7.2 synchronized实现

    在普通方法上添加关键字

    public synchronized void recover() {
        hp += 1;
    }

    添加synchronized代码块

       public  void hurt() {
    //        需要声明 线程 访问的是哪一个同步对象
            synchronized (this){
                hp -= 1;
            }
        }

    3.8 线程死锁

    导致程序无法继续进行 并且会大量消耗性能

    死锁

    //三个进程的死锁
    public static void main(String[] args) {
            Monster m1 = new Monster();
            Monster m2 = new Monster();
            Monster m3 = new Monster();
    
            m1.name = "程序媛";
            m2.name = "程序猿";
            m3.name = "程序员";
    
    
            Thread t1 = new Thread(){
                public void run(){
    //                线程1想先占有m1的锁
                    synchronized (m1){
                        System.out.println("t1 占有了m1的锁");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("t1试图占有m2的锁");
                        System.out.println("t1正在等待。。。");
                        synchronized (m2){
                            System.out.println("t1成功占有m2的锁");
                        }
                    }
                }
            };
    
            Thread t2 = new Thread(){
                public void run(){
    //                线程2想先占有m2的锁
                    synchronized (m2){
                        System.out.println("t2 占有了m2的锁");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("t2试图占有m3的锁");
                        System.out.println("t2正在等待。。。");
                        synchronized (m3){
                            System.out.println("t2成功占有m3的锁");
                        }
                    }
                }
            };
    
            Thread t3 = new Thread(){
                public void run(){
    //                线程2想先占有m2的锁
                    synchronized (m3){
                        System.out.println("t3 占有了m3的锁");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("t3试图占有m1的锁");
                        System.out.println("t3正在等待。。。");
                        synchronized (m1){
                            System.out.println("t3成功占有m1的锁");
                        }
                    }
                }
            };
            t1.start();
            t2.start();
            t3.start();
    }

    3.9 线程调度方法

    线程交互三种方法

    wait notify notifyAll

    并不是通过线程调用的

    而是通过同步对象调用的 这三个方法都是Object中所定义的方法

    因为 任意对象 都可以被作为同步对象使用 所以方法只能定义在Object里

    3.9.1 wait

    让占有当前同步对象锁的线程 进行等待 并且释放锁

    处于wait状态的线程 如果不通过同步对象调用notify方法去唤醒 他就会一致处于wait状态

    wait的使用是有条件的 一般来说 wait必须出现在synchronized代码块中

    3.9.2 notify

    notify 通知一个处于wait状态的线程 让其苏醒过来 重新竞争jvm资源 获取锁 运行代码

    notify一次只能唤醒一个线程

    notifyAll 通知所有在该同步对象上等待的线程 苏醒 他们都有机会重新去竞争

    同步对象锁 继续运行了

    这两个方法是基于同步对象的 不是基于线程的

    3.9.3 notifyAll

    唤醒全部处于wait状态的线程

    3.10 生产者消费者模型

    生产者-消费者模式是一个经典的多线程设计模式

    若干个生产者线程 若干个消费者线程

    生产者线程负责提交用户请求 消费者线程负责处理用户线程

    生产者消费者模型例子

    定义一个馒头类

    public class SteamedBun {
        public int id;
    
        public SteamedBun(int id) {
            this.id = id;
        }
    
        public SteamedBun() {
        }
    
        @Override
        public String toString() {
            return "SteamedBun{" +
                    "id=" + id +
                    '}';
        }
    }

    定义一个篮子类

    用来装馒头

    定义一个容器(有长度限制)来装馒头

    篮子需要实现取馒头 装馒头两个方法

    public class Basket {
        int index = 0;
    //    定义馒头容器
    //    假设篮子只能装6个馒头
        SteamedBun[] arrBun = new SteamedBun[6];
    
        public synchronized void push(SteamedBun bun){
    //        如果篮子满了,则装馒头的线程进入到wait状态
    //        用循环体保证  只要篮子是满的   装馒头的线程只能处于wait状态
            while(index == arrBun.length){
                try {
                    System.out.println("篮子满了");
                    Thread.sleep(1000);
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    //        如果有馒头取出,没有馒头了  唤醒装馒头进程
            this.notify();
    //        把当前对象存放到篮子容器里
            arrBun[index] = bun;
            index ++;
        }
    
        public synchronized SteamedBun pull(){
    //        篮子空了
            while (index == 0){
                try {
                    System.out.println("篮子空了");
                    Thread.sleep(1000);
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    //        如果装馒头的wait  唤醒装馒头线程
            this.notify();
            index--;
            return arrBun[index];
        }
    }

    定义生产者

    生产馒头 同时要获取篮子对象

    public class Producer implements Runnable{
    //    为了方便获取篮子  在类中定义一个篮子类的引用
        Basket bt = null;
    
        public Producer(Basket bt) {
            this.bt = bt;
        }
    
        @Override
        public void run() {
    //        假设一共要放20个馒头
            for (int i = 0; i < 20; i++) {
                SteamedBun bun = new SteamedBun(i);
    //            利用篮子push方法装到篮子中
                bt.push(bun);
                System.out.println("生产者 生产了馒头:"+bun);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    定义一个消费者

    获取篮子对象

    必须保证生产者和消费者获取到的篮子是同一个 才能保证数据共享

    public class Consumer implements Runnable {
        Basket bt = null;
    
        public Consumer(Basket bt) {
            this.bt = bt;
        }
    
        @Override
        public void run() {
    //        一共取20个
            for (int i = 0; i < 20; i++) {
                SteamedBun bun = bt.pull();
                System.out.println("消费者消费了:"+bun);
            }
        }
    }

    测试生产者消费者模型

        public static void main(String[] args) {
    //        定义篮子
            Basket bt = new Basket();
    //        定义生产者
            Producer producer = new Producer(bt);
    //        定义消费者
            Consumer consumer = new Consumer(bt);
            new Thread(producer).start();
    
    //        为了给生产者足够的时间去生产馒头
            try {
                Thread.sleep(8000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(consumer).start();
    
        }

    3.11 线程池

    为什么需要线程池?

    5000个用户同时(在一个时间区域段)访问

    不做优化的情况下 每一个用户进行访问 都会开启一个线程

    线程服务完用户之后 会闲置 占用jvm资源

    如果能定义一个线程池 每当有用户进行业务处理 则从池中取出一个线程

    多线程实现思路

    1. 准备一个任务容器
    2. 一个启动n个消费者线程
    3. 刚开始任务任务容器是空的,所有线程都wait在上面
    4. 知道一个外部线程往这个容器中扔了一个”任务“,就会有一个消费者线程被notify唤醒
    5. 这个消费者线程取出”任务“,并且执行任务,执行完毕后,继续等待下一次任务的到来。

    线程池

    3.11.1 线程池的实现方式

    自己实现线程池

    ThreadPool类

    public class ThreadPool {
        //    线程池大小
        int threadPoolSize;
    
        //    任务容器  任务容器中的任务 对应也用线程表示
        LinkedList<Runnable> tasks = new LinkedList();
    
        //    完成线程的初始化
        public ThreadPool() {
    //        初始化线程池大小
            threadPoolSize = 10;
    //        往线程池中放入十个工具人线程  并且在实例化线程对象之后start()
            synchronized (tasks) {
                for (int i = 0; i < threadPoolSize; i++) {
                    new TaskConsumerThread("任务消费者线程(负责消费线程)" + i).start();
    
                }
            }
        }
    
        //    向任务容器中添加任务
        public void add(Runnable r) {
            synchronized (tasks) {
    //            我们在初始化线程池的时候  里面的10个工具人线程
    //            都被我们通过同步对象tasks wait了
    //            为了保证一会任务添加之后  工具人能够去消费任务
                tasks.add(r);
    //            唤醒所有处于wait状态的工具人线程
                tasks.notifyAll();
            }
        }
    
    
        //    使用匿名内部类创建 消费者线程
        class TaskConsumerThread extends Thread {
            //        重写Thread的构造方法 方便给线程对象定义name属性
            public TaskConsumerThread(String name) {
                super(name);
            }
    
            //        定义任务属性
            Runnable task;
    
            @Override
            public void run() {
                System.out.println(this.getName() + "启动了");
                while (true) {
    //                获取tasks任务容器的锁 避免某一个工具人线程在从任务容器tasks里获取任务的时候
    //                导致多个线程获取同一个任务的情况出现
                    synchronized (tasks) {
    //                    只要任务容器是空的 通过wait方法 让所有的线程处于wait状态
                        while (tasks.isEmpty()) {
                            try {
                                tasks.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
    //                    任务容器不为空 当前负责消费任务的工具人线程就可以干活
    //                    从任务容器中 取出一个任务 存放在task引用里
                        task = tasks.removeLast();
    //                    允许添加任务的线程 可以继续添加任务
                        tasks.notifyAll();
                    }
                    System.out.println(this.getName() + "成功获取到任务,并执行");
    //                任务执行
                    task.run();
                }
            }
        }
    }

    TestThreadPool测试类

    任务线程的实现写在了测试类中

    public class TestThreadPool {
        public static void main(String[] args) {
    //        先实例化一个线程池对象
    //        十条工具人线程已经在线程池对象被实例化的同时启动了
            ThreadPool pool = new ThreadPool();
    
    //        定义若干个任务线程 并且定义任务线程执行的方法内容
            for (int i = 0; i < 5; i++) {
                Runnable task = new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("我是一个任务线程,我执行任务");
                    }
                };
    //            把定义的任务线程添加到线程池中定义好的任务容器中
                pool.add(task);
            }
    
    //        延长主线程的运行时间  给我们的线程池中的线程 以及其它线程足够的时间运行完自己的代码
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    java自带线程池

    性能更好

    ThreadPoolExcutor()中的参数依次表示为:

    corePoolSize:线程的数量

    maximunPoolSize:最大能容纳线程数量

    keepAliveTime:线程能存活的时间,超过这个时间,则将线程wait

    TimeUnit: 表示给定单元粒度的时间段

    主要作用

    • 时间颗粒度转换
    • 延时
    • 常用颗粒度
    TimeUnit.DAYS     ``//天
    TimeUnit.HOURS     ``//小时
    TimeUnit.MINUTES    ``//分钟
    TimeUnit.SECONDS    ``//秒
    TimeUnit.MILLISECONDS ``//毫秒
    • 时间颗粒度转换
    public` `long` `toMillis(``long` `d)  ``//转化成毫秒
    public` `long` `toSeconds(``long` `d) ``//转化成秒
    public` `long` `toMinutes(``long` `d) ``//转化成分钟
    public` `long` `toHours(``long` `d)  ``//转化成小时
    public` `long` `toDays(``long` `d)   ``//转化天

    例子

        ``//1天有24个小时  1代表1天:将1天转化为小时
        ``System.out.println( TimeUnit.DAYS.toHours( ``1` `) );

    TimeUnit实现延时

    TimeUnit.SECONDS.sleep(5);//延时五秒
    //等同于
    Thread.sleep(5000);

    BlockingQueue:任务队列, 双缓冲队列。内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。

    • 常用的缓冲队列

      • ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
      • LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
      • PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
      • SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。
    public static void main(String[] args)throws Exception {
            ThreadPoolExecutor threadPool =
              new ThreadPoolExecutor(10,15,60,
                      TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
    
    //        向java线程池中添加任务 由其执行
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务1");
                }
            });
        }

    4 Socket编程

    ip地址:代表的是计算机位于网络中的位置

    ​ 在公网中,公网ip地址是不允许重复的

    ​ 127.0.0.1 固定ip 代表的是当前计算机 this

    服务器 客户端

    基于端口进行通信

    每一个设备上 一个端口只能对应一个应用程序

    端口也是访问设备的标识符之一

    B/S

    Browser/Server

    优:客户端成本低 维护成本低 易使用

    缺:本身浏览器的结构决定应用场景

    C/S

    Client/Server

    优:减少数据通信的成本 很多运行时需要的资源都存放在客户端

    缺:使用麻烦 维护成本高 要做不同系统的适配版

    网络测试 ping 用来测试目标ip响应时间

    java中主要依靠socket套接字来进行不同程序之间的通信

    4.1 实现windows程序

        public static void main(String[] args) throws IOException {
    //        在java中运行windows的exe程序
            Process p = null;
            try {
                p = Runtime.getRuntime().exec("ping 202.108.22.5");
            } catch (IOException e) {
                e.printStackTrace();
            }
    //        通过缓存流读取exe程序运行之后所返回的数据
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line = null;
            StringBuilder sb = new StringBuilder();
            while((line = br.readLine()) !=null){
                if(line.length() != 0){
                    sb.append(line+"\r\n");
                }
            }
            System.out.println(sb);
        }

    4.2 ServerSocket服务器端/Socket

    数据流在字节流的基础上 封装数据流 通过数据流进行数据传输

    public class Server {
        public static void main(String[] args) {
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                Connection c = DriverManager.getConnection("jdbc:mysql://101.34.70.96:3306/jerryDb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
                        "admin", "123456");
                String sql = "select * from reply where request=?";
                Random random = new Random();
    
    //                新建服务器端 并且开放对应端口
                ServerSocket ss = new ServerSocket(8848);
    //                服务器许哟啊对指定端口进行监听
                System.out.println("正在监听端口:8848");
                Socket s = ss.accept();
    //            System.out.println("有客户端连接接入");
    //            打开输入流
                InputStream is = s.getInputStream();
                OutputStream os = s.getOutputStream();
                DataInputStream dis = new DataInputStream(is);
    //            添加输出流  方便服务器端向客户端发送消息
                DataOutputStream dos = new DataOutputStream(os);
    //            读取客户端发送的数据
                while (true) {
                    PreparedStatement ps = c.prepareStatement(sql);
                    ps.setString(1, dis.readUTF());
                    ResultSet rs = ps.executeQuery();
                    String reply = null;
                    if (rs.next()) {
                        reply = rs.getString("replymessage");
                    } else {
                        reply = "亲,听不懂您在说啥哦";
                    }
                    dos.writeUTF(reply);
    
                }
    
    
    //            s.close();
    //            ss.close();
    //            dis.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public class Client {
        public static void main(String[] args) {
            try{
                Socket s = new Socket("127.0.0.1",8848);
    //            System.out.println(s);
                OutputStream os = s.getOutputStream();
                InputStream is = s.getInputStream();
    //            在字节流的基础上  封装数据流  通过数据流进行数据传输
    //            可以比较方便的发送字符串以及其它数据类型的数据
                DataOutputStream dos = new DataOutputStream(os);
                DataInputStream dis = new DataInputStream(is);
                while(true){
    //            通过scanner输入发送数据
                    Scanner scanner = new Scanner(System.in);
                    String message = scanner.nextLine();
    //            通过数据流的writeUTF
                    dos.writeUTF(message);
                    System.out.println(dis.readUTF());
                }
    
    //            发一段数字给服务器
    //            os.write(66);
    //            os.close();
    //            dos.close();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    客户端和服务器端要实现能够同时发送和接收

    客户端和服务器端分别都需要至少一条接收消息线程和一条发送消息线程

    public class SendThread extends Thread{
        private Socket s;
        public SendThread(Socket s){
            this.s = s;
        }
    
        @Override
        public void run(){
            try{
                OutputStream os = s.getOutputStream();
                DataOutputStream dos = new DataOutputStream(os);
                while(true){
                    Scanner sc = new Scanner(System.in);
                    dos.writeUTF(sc.nextLine());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public class ReceiveThread extends Thread {
        private Socket s;
        public ReceiveThread(Socket s){
            this.s=s;
        }
    
        @Override
        public void run(){
            try{
                InputStream is = s.getInputStream();
                DataInputStream dis = new DataInputStream(is);
                while(true){
                    System.out.println(dis.readUTF());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public class Server {
        public static void main(String[] args) throws IOException {
            ServerSocket ss = new ServerSocket(8888);
            System.out.println("正在监听端口8888");
            Socket s = ss.accept();
            new SendThread(s).start();
            new ReceiveThread(s).start();
        }
    }
    public class Client {
        public static void main(String[] args) throws IOException {
            Socket s = new Socket("127.0.0.1",8888);
            new SendThread(s).start();
            new ReceiveThread(s).start();
        }
    }

    5 反射

    理解反射之前 理解类对象

    类对象:所有的类 都存在一个对应的类对象 这个类用于提供类本身的信息 有哪些方法 有哪些属性 有哪些构造方法 Class.class

    类对象就是用来描述 每一个类有哪些属性和哪些方法的

    一个jvm中 一种类 只会有一个类对象

    约定优于配置 配置优于实现

    5.1 获取对象方式

    5.1.1 通过类的路径名称

    //        Class class1 = Class.forName(classname);

    5.1.2 通过 .class 类文件直接获取

    //        Class class2 = Student.class;

    5.1.3 通过调用getClass方法进行获取

    //        Class class3 = new Student().getClass();

    三种方式获取到的类对象都是同一个类对象

    当我们获取类的对象时,会导致类的属性被初始化

    除了 .class的方式 都会初始化类的属性

    5.2 通过反射的方式创建对象

    //传统方式
                Student s = new Student();
            s.setName("jerry");

    反射方式

    反射方式中 引入了第二个反射对象 Constructor
    将构造方法 当作对象来进行处理
    获取类对象

    Class class1 = Class.forName("Test2.Student");

    根据类对象获取它的构造方法所对应的构造方法对象

    Constructor c = class1.getConstructor();
    Student s1 = (Student)c.newInstance();
    s1.setName("bob");
    System.out.println(s1.getName());

    5.3 通过反射访问属性

    在反射中 对象的属性也被封装了对象 Field对象

        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    //        所有的反射都需要类对象进行获取
            Student s = new Student();
            s.setName("jerry");
    //        先获取类对象
            Class class1 = s.getClass();
    //        根据类对象获取属性对象Field
            Field f1 = class1.getDeclaredField("name");
    //        修改对象的属性值
            f1.set(s,"女装jerry");
    //        验证对象的属性被修改
            System.out.println(s.getName());
        }

    5.4 通过反射调用方法

    获取方法的时候 需要同时提供方法的名称 以及方法的参数所对应的类对象

       public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //        获取类对象
            Class class1 =Student.class;
    //        通过反射获取Student类对象
            Constructor c = class1.getConstructor();
            Student s = (Student) c.newInstance();
    //        反射将方法也封装成了对象  Method类的对象
    //        获取方法的时候  需要同时提供方法的名称 以及方法的参数所对应的类对象
            Method m = class1.getDeclaredMethod("setName", String.class);
    //        借助 m 调用方法
            m.invoke(s,"jerry");
            System.out.println(s.getName());
        }

    5.5 反射的应用

    • spring 依赖注入 控制反转 完全依赖反射实现的 框架的底层代码之一
    • 不需要通过new创建代码

    5.6 通过Properties配置文件实例对象

    配置文件properties.txt

    class=Test4.Services2
    Method=run2
    public class Services1 {
        public void run1(){
            System.out.println("假装执行了一个业务方法");
        }
    }
    public class Services2 {
        public void run2(){
            System.out.println("我也假装运行了一些业务方法");
        }
    }
    public class TestReflect {
    //    根据反射和IO流 读取配置文件中的信息
    //    通过反射完成对象创建和方法调用
    //    后续如果需要更改方法 不需要修改代码  只需要修改配置文件
    public static void main(String[] args) throws Exception {
        File configFile = new File("src/properties.txt");
    //    System.out.println(configFile.exists());
    //    java中提供了一个类  叫做Properties类 转么能用来通过io流方便快捷的读取文本数据
        Properties config = new Properties();
        config.load(new FileInputStream(configFile));
    //    从Properties对象中  读取想要的配置信息
        String className = (String)config.get("class");
        String methodName = (String)config.get("Method");
    //    读取属性是为了后续的反射服务
    //    根据类名称获取类对象
        Class class1 = Class.forName(className);
    //    根据方法名称获取方法对象
        Method m = class1.getMethod(methodName);
    //    根据类对象 获取类对象的构造器对象
        Constructor c = class1.getConstructor();
    //    根据构造器对象 实例化对应的对象
        Object services = c.newInstance();
    //    调用对象的指定方法
        m.invoke(services);
    }
    }

    6 Servlet

    6.1 Servlet基本介绍

    Servlet 本身不能独立运行,需要在一个web应用中运行的 而一个web应用是部署在tomcat中的

    所以开发一个servlet需要如下几个步骤 在idea中借助maven创建web应用项目 编写servlet代码 部署到idea中配置好的tomcat容器中 所以在真正使用servlet之前我们需要做好前期的准备工作

    6.1.2 环境配置

    6.1.2.1 maven项目搭建

    新建项目时 选 择以maven项目进行配置 选择

    org.apache.maven.archetypes:maven-archetype-webapp

    检查maven配置

    项目位置确认

    tomcat服务器配置

    VM option:-Dfile.encoding=utf-8

    URL和HTTP port端口保持一致

    Deployment中创建实例 上下文设置

    6.2 HelloServlet示例

    6.2.1 编写第一个Servlet

    编写Servlet类 在编写的时候需要进行maven进行依赖的加载

    package com.iweb;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Date;
    
    public class HelloServlet extends HttpServlet {
        public void doGet(HttpServletRequest request, HttpServletResponse response){
    
            try {
                response.getWriter().println("<h1>Hello Servlet!</h1>");
                response.getWriter().println(new Date().toLocaleString());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    6.2.1.1 maven进行依赖的加载

    1. 官网导入
    2. alt+insert 导入依赖 Depency

    6.2.1.2 web.xml的配置

    web.xml提供路径与servlet的映射关系 把/hello这个路径,映射到 HelloServlet这个类上

    <servlet> 标签下的 <servlet-name> 与 <servlet-mapping> 标签下的 <servlet-name> 必须一样

    <servlet-name>与<servlet-class>可以不一样,但是为了便于理解与维护,一般都会写的一样。 一目了然

    <!DOCTYPE web-app PUBLIC
     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
     "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
    <web-app>
      <display-name>Archetype Created Web Application</display-name>
      <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.iweb.HelloServlet</servlet-class>
      </servlet>
    
      <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
      </servlet-mapping>
    </web-app>

    sevrletwebxml

    启动服务器 输入对应的路径

    6.3 请求参数的获取

    html登录页面提供

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <!--  在浏览器中设置中文  -->
    <!--    告诉浏览器 在一会发送-->
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <title>Title</title>
    </head>
    <body>
    <!--如果希望在向后端发送请求的时候 能够携带参数数据 这个时候需要提供一个元素form表单-->
    <!--一会我们会将form中的数据 进行提交 login就是我们的提交的uri 和第一个demo中的/test1类似
    当我们提交表单的时候 请求的方式为 post 我们应该在对应的 Servlet 提供一个doPost方法来进行处理-->
    <form action="login" method="post">
    用户:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
        <input type="submit" value="登录">
    </form>
    </body>
    </html>

    创建一个loginServlet

    package com.iweb;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class LoginServlet extends HttpServlet {
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String name = request.getParameter("name");
            String password = request.getParameter("password");
    
            System.out.println("name:" + name);
            System.out.println("password:" + password);
        }
    }

    映射LoginServlet到路径login

    由于idea的特性 所有的servet标签和mapping标签必须分开编写

    <!DOCTYPE web-app PUBLIC
     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
     "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
    <web-app>
      <display-name>Archetype Created Web Application</display-name>
      <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.iweb.HelloServlet</servlet-class>
      </servlet>
      <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.iweb.LoginServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
      </servlet-mapping>
      <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
      </servlet-mapping>
    </web-app>

    6.4 响应返回

    修改LoginServlet

    增加密码验证部分 判断账号密码是否为 abaaba 123,如果是就打印 success 否则就打印 fail 根据账号密码,创建对应的html字符串。 然后通过response.getWriter().println() 发送到浏览器。

    package com.iweb;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class LoginServlet extends HttpServlet {
    
            protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
                String name = request.getParameter("name");
                String password = request.getParameter("password");
                String html = null;
                if ("abaaba".equals(name) && "123".equals(password))
                    html = "<div style='color:green'>success</div>";
                else
                    html = "<div style='color:red'>fail</div>";
                PrintWriter pw = response.getWriter();
                pw.println(html);
    
            }
        }

    6.5 Servlet调用过程

    servlet调用流程

    http://127.0.0.1:8080/helloServlet/login.html

    打开一个静态的html页面,在这个页面中可以通过form,以post的形式提交数据

    在login.html中,用form,把账号和密码,提交到/login这个路径,并且附带method="post"

    Tomcat 定位到了LoginServlet后,发现并没有LoginServlet的实例存在,于是通过读取到的web.xml中的匹配标签,读取的class的路径,通过反射实例化Servlet对象

    Tomcat从上一步拿到了LoginServlet的实例之后,根据页面login.html提交信息的时候带的method="post",去调用对应的doPost方法。

    接着流程进入了doPost方法中,

    protected void doPost(HttpServletRequest request, HttpServletResponse response){
    
    }

    在这个方法中,通过参数request,把页面上传递来的账号和密码信息取出来

    String name = request.getParameter("name");
    String password = request.getParameter("password");

    接着,根据账号和密码是否正确(判断是否是admin和123), 创建不同的html字符串。

    然后把html字符串通过如下方式,设置在了response对象上。

    PrintWriter pw = response.getWriter(); pw.println(html);

    到这里,Servlet的工作就做完了。

    在Servlet完成工作之后,tomcat拿到被Servlet修改过的response,根据这个response生成html 字符串,然后再通过HTTP协议,这个html字符串,回发给浏览器,浏览器再根据HTTP协议获取这个html字符串,并渲染在界面上。

    这样在效果上,浏览器就可以看到Servlet中生成的字符串了。

    6.6 Service方法解析

    6.6.1 doGet()

    当浏览器使用get方式提交数据的时候,servlet需要提供doGet()方法 以下几种方式是使用get方式提交的 form默认的提交方式 如果通过一个超链访问某个地址 如果在地址栏直接输入某个地址 ajax指定使用get方式的时候

    import java.io.IOException;
     
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class LoginServlet extends HttpServlet {
     
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
     
        }
     
    }

    6.6.2 doPost()

    当浏览器使用post方式提交数据的时候,servlet需要提供doPost()方法 以下几种方式是使用post方式提交的

    在form上显示设置 method="post"的时候 ajax指定post方式的时候

    import java.io.IOException;
     
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class LoginServlet extends HttpServlet {
     
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
     
        }
     
    }

    6.6.3 service()

    LoginServlet继承了HttpServlet,同时也继承了一个方法

    service(HttpServletRequest , HttpServletResponse )

    实际上,其实是在在service中通过 req.getMethod() 对传过来的方法进行了判断,随后根据不同方法调用不同的doget,dopost等.

    部分源码解析如下

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String protocol = req.getProtocol();
            String msg = lStrings.getString("http.method_post_not_supported");
            if (protocol.endsWith("1.1")) {
                resp.sendError(405, msg);
            } else {
                resp.sendError(400, msg);
            }
     
        }
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String protocol = req.getProtocol();
            String msg = lStrings.getString("http.method_get_not_supported");
            if (protocol.endsWith("1.1")) {
                resp.sendError(405, msg);
            } else {
                resp.sendError(400, msg);
            }
     
        }

    6.7 中文处理

    为了成功获取中文参数,需要做如下操作

    1. login.html中加上

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

    这句话的目的是告诉浏览器,等下发消息给服务器的时候,使用UTF-8编码

    2.在servlet中定义请求的数据编码格式 在reqeust被使用之前 加上这一句 就可以避免编码问题

       request.setCharacterEncoding("UTF-8");

    返回中文的响应

    在Servlet中,加上

    response.setContentType("text/html; charset=UTF-8");

    就可以在Servlet中通过响应向页面返回内容的时候,指定字符串的字符集编码。

    6.8 Servlet生命周期

    实例化------->初始化(init)----------->提供服务(Service)------------>销毁(destroy)---------->回收(GC)

    无论访问了多少次LoginServlet LoginServlet构造方法 只会执行一次,所以Servlet是单实例

    初始化

    LoginServlet 继承了HttpServlet,同时也继承了init(ServletConfig) 方法 init 方法是一个实例方法,所以会在构造方法执行后执行。 无论访问了多少次LoginSerlvet init初始化 只会执行一次

    Servlet 作为 web容器 的一个组件,在一个用户访问某个容器时,会通过开启一个线程单实例开启一个 Servlet,这时创建一个 Servlet ,而 init() 方法是作为某个实例的 Servlet 的方法,在整个 Servlet 生命周期被调用一次,它们的执行时间是不同的,这要根据客户访问时间来作为依据。

    除此之外,还有个很重要的原因,init() 是可以重写的,但初始化时 web容器 帮你做的,你根本无需关心初始化过程,但有些业务却需要你在 Servlet 提供服务之前就要做,这时可以通过 init() 来写你需要的业务代码

    提供服务

    接下来就是执行service()方法,然后通过浏览器传递过来的信息进行判断,是调用doGet()还是doPost()方法

    在service()中就会编写我们的业务代码

    销毁

    关闭tomcat的时候 destroy()方法会被调用,但是这个一般都发生的很快,不易被发现。

    被回收

    当该Servlet被销毁后,就满足垃圾回收的条件了。 当下一次垃圾回收GC来临的时候,就有可能被回收。 这个几乎观察不到,毕竟GC是守护线程,耶稣来了都不知道它啥时候工作 所以就当无事发生。

    6.9 转发和重定向

    首先在webapp目录下准备两个页面 success.html,fail.html 分别用于显示登录成功 或者登录失败

    如果登录成功了,就服务端跳转到success.html

    如果登录失败了,就客户端跳转到fail.html

    6.9.1 转发方式

    import java.io.IOException;
     
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class LoginServlet extends HttpServlet {
    
     
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
     
            String name = request.getParameter("name");
            String password = request.getParameter("password");
     
            if ("abaaba".equals(name) && "123".equals(password)) {
                request.getRequestDispatcher("success.html").forward(request, response);
            }
     
        }
     
    }

    6.9.2 重定向方式

    可以很明显的观察到 浏览器地址栏的变化

    import java.io.IOException;
     
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class LoginServlet extends HttpServlet {
     
        private static final long serialVersionUID = 1L;
     
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
     
            String name = request.getParameter("name");
            String password = request.getParameter("password");
     
            if ("abaaba".equals(name) && "123".equals(password)) {
                request.getRequestDispatcher("success.html").forward(request, response);
            }
            else{
                response.sendRedirect("fail.html");
            }
     
        }

    6.9.3 两者区别

    转发与重定向区别

    6.10 Servlet自启动

    有的时候会有这样的业务需求: tomcat一启动,就需要执行一些初始化的代码,比如校验数据库的完整性等。

    但是Servlet的生命周期是在用户访问浏览器对应的路径开始的。如果没有用户的第一次访问,就无法执行相关代码。

    这个时候,就需要Servlet实现自启动 即,伴随着tomcat的启动,自动启动初始化,在初始化方法init()中,就可以进行一些业务代码的工作了。

    在web.xml中,配置Hello Servlet的地方,增加一句

    <load-on-startup>10</load-on-startup>

    取值范围是1-99

    即表明该Servlet会随着Tomcat的启动而初始化。

    同时,为HelloServlet提供一个init(ServletConfig) 方法,验证自启动

    如图所示,在tomcat完全启动之前,就打印了init of HelloServlet

    <load-on-startup>10</load-on-startup>

    中的10表示启动顺序 如果有多个Servlet都配置了自动启动,数字越小,启动的优先级越高

    6.11 request 对象常见方法

    request.getRequestURL(): 浏览器发出请求时的完整URL,包括协议 主机名 端口(如果有)"
    request.getRequestURI(): 浏览器发出请求的资源名部分,去掉了协议和主机名"
    request.getQueryString(): 请求行中的参数部分,只能显示以get方式发出的参数,post方式的看不到
    request.getRemoteAddr(): 浏览器所处于的客户机的IP地址
    request.getRemoteHost(): 浏览器所处于的客户机的主机名
    request.getRemotePort(): 浏览器所处于的客户机使用的网络端口
    request.getLocalAddr(): 服务器的IP地址
    request.getLocalName(): 服务器的主机名
    request.getMethod(): 得到客户机请求方式一般是GET或者POST 

    参数获取 常用的是

    request.getParameter(): 是常见的方法,用于获取单值的参数
    request.getParameterValues(): 用于获取具有多值的参数
    request.getParameterMap(): 用于遍历所有的参数,并返回Map类型。

    下面是简单的参数获取的应用实例

    首先准备register.html

    <form action="register" method="get">
        用户名 : <input type="text" name="name"> <br>
        喜好 : hitSword<input type="checkbox" name="hobits" value="hitSword">
            boxing<input type="checkbox" name="hobits" value="boxing"> <br>
           
             <input type="submit" value="注册">
    </form>

    然后准备对应的Servlet类

    mport java.io.IOException;
    import java.util.Arrays;
    import java.util.Map;
    import java.util.Set;
     
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class RegisterServlet extends HttpServlet {
     
        protected void service(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
     
            System.out.println("获取单值参数name:" + request.getParameter("name"));
     
            String[] hobits = request.getParameterValues("hobits");
            //Arrays是数组的一个封装类 可以通过他的asList方法方便的遍历数组
            System.out.println("获取具有多值的参数hobits: " + Arrays.asList(hobits));
     
    
            Map<String, String[]> parameters = request.getParameterMap();
            //keySet方法可以直接获取到HashMap的key的集合 存入到一个Set集合中
            //后续只需要通过增强for循环对key进行遍历 就可以显示所有的value了
            Set<String> paramNames = parameters.keySet();
            for (String param : paramNames) {
                String[] value = parameters.get(param);
                System.out.println(param + ":" + Arrays.asList(value));
            }
     
        }
     
    }

    6.12 response响应对象常用方法

    6.12.1 设置响应内容

    PrintWriter pw= response.getWriter();

    通过response.getWriter(); 获取一个PrintWriter 对象

    可以使用println(),append(),write(),format()等等方法设置返回给浏览器的html内容。

    import java.io.IOException;
    import java.io.PrintWriter;
     
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class HelloServlet extends HttpServlet{
         
        public void doGet(HttpServletRequest request, HttpServletResponse response){
             
            try {
                PrintWriter pw= response.getWriter();
                pw.println("<h1>Hello Servlet</h1>");
                 
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
         

    6.12.2 设置响应格式

    response.setContentType("text/html");

    "text/html" 是即格式,,表示浏览器可以识别这种格式,如果换一个其他的格式, 比如 "text/jijian" ,浏览器不能识别,那么打开此servlet就会弹出一个下载的对话框

    示例代码如下

    import java.io.IOException;
    import java.io.PrintWriter;
     
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
     
    public class HelloServlet extends HttpServlet {
     
        public void doGet(HttpServletRequest request, HttpServletResponse response) {
     
            try {
                PrintWriter pw = response.getWriter();
                pw.println("<h1>Hello Servlet</h1>");
     
                response.setContentType("text/jijian");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
     
    }

    6.12.3 设置响应编码

    设置响应编码有两种方式

     response.setContentType("text/html; charset=UTF-8");
     response.setCharacterEncoding("UTF-8");

    他们的区别在于

    第一种方式不仅发送到浏览器的内容会使用UTF-8编码,而且还通知浏览器使用UTF-8编码方式进行显示。所以总能正常显示中文

    第二种方式仅仅是发送的浏览器的内容是UTF-8编码的,至于浏览器是用哪种编码方式显示不管。 所以当浏览器的显示编码方式不是UTF-8的时候,就会看到乱码,需要手动再进行一次设置

    7 JSP

    7.1 jsp简介

    java server page 页面显示数据的方式

    jsp java server page 顶峰的时候在0几年 那个时候和微软的asp全家桶(SqlServer .net) 占据了web开发的主流 很多人选择了asp之后 再也没有然后了 当然 jsp也在随后的web开发的发展中逐渐被替代(不是被淘汰 而是本身性能有一些差) 但是并不影响我们对jsp技术的学习 现阶段的角度 jsp还是一个很不错的页面技术

    废话不多说了 先说一说为什么要用jsp

    从我们昨天对servlet的使用中可以感觉到 纯servlet在进行页面跳转的时候 只能跳转到静态页面(html) 如果需要从后端获取显示数据到页面中的时候 需要进行通过

    resp.getWriter().println()

    7.2 jsp简单应用

    首先 构建maven web项目 配置tomcat服务器 并提供必要的目录 这个之前第一天已经详细说过了 不过多赘述

    然后在maven添加相关依赖 并且通过maven reimport确保jar包可以正确 注意 使用jsp需要额外引入jsp相关jar包

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency> 
       <groupId>javax.servlet.jsp</groupId> 
       <artifactId>jsp-api</artifactId> 
       <version>2.1</version> 
       <scope>provided</scope>
    </dependency>

    然后 新建一个jsp 并进行简单应用

    <%@page contentType="text/html; charset=UTF-8"  pageEncoding="UTF-8" import="java.util.*"%>

    这行代码代表的是jsp的<%page指令 后续会详细介绍

    contentType="text/html; charset=UTF-8" 

    相当于是我们servlet中的

    response.setContentType("text/html; charset=UTF-8"); 通知浏览器以UTF-8进行中文解码

    而这一行

    pageEncoding="UTF-8" 

    代表如果jsp文件中出现了中文,这些中文使用UTF-8进行编码

    import="java.util.* 

    导入其他类,如果导入多个类,彼此用,逗号隔开,像这样 import="java.util.,java.sql."

    为啥可以像java一样进行导包呢?因为jsp的本质还是Servlet 还是java类 原理层后续会进行描述

    <%=new Date().toLocaleString()%>

    <%= 相当于是在Servlet中使用resp.getWriter()通过流进行输出 可以在页面直接显示数据内容

    7.3 jsp执行流程

    7.3.1 原理

    本质上他就是个一个jsp

    但是每一个jsp从编写好到运行经历了数个流程

    jsp执行原理

    1. 把 hello.jsp转译为hello_jsp.java
    2. hello_jsp.java 位于

    每个人因为系统不同和idae安装位置不同 可以通过搜索转译之后的java文件名在本地搜索得到

    3.把hello_jsp.java 编译为hello_jsp.class

    4.执行hello_jsp,生成html

    5.通过http协议把html 响应返回给浏览器

    6.用户从浏览器看到最终的页面

    7.4 jsp页面元素

    jsp由这些页面元素组成:

    7.4.1 静态内容

    就是html,css,javascript等内容

    7.4.2 指令

    以<%@开始 %> 结尾,比如<%@page import="java.util.*"%>

    7.4.3 表达式 <%=%>

    用于输出一段html

    7.4.4 Scriptlet

    在<%%> 之间,可以写任何java 代码

    7.4.5 动作

    <jsp:include page="Filename" > 在jsp页面中包含另一个页面。后续会详细说明

    7.4.6 注释 <%-- -- %>

    不同于 html的注释 通过jsp的注释,浏览器也看不到相应的代码,相当于在servlet中注释掉了

    7.5 jsp include

    在我们进行页面编辑的时候,很多页面会出现很多重复的页面 比如页头的公共导航栏 或者是页脚的版权声明或者是合作推广 如果是每开发一个页面 都需要进行相关的 和页面编写 费时费力 不符合开发者偷懒(高效开发)的要求 所以 引入了jspinclude 通过页面引入进行代码服用 提高代码复用率

    如何去使用?

    先构建一个footer.jsp 代表页脚 方便一会的页面引入

    代码很简单 新建一个footer.jsp 随便准备一点数据即可

    <hr>
    <p style="text-align:center">版权所有,侵权就侵权吧@2021
    </p>

    然后在任意界面里通过引入的方式进行代码复用 jsp一共有两种页面包含方式 一种是指令include 代码如下

    随便新建一个jsp

    通过指令include方式引入footer.jsp

     <%@include file="footer.jsp" %>

    动作include 代码如下

    <jsp:include page="footer.jsp" />

    两者的区别

    通过之前的学习知道,JSP最后会被转译成Servlet

    如果是指令include

    <%@include file="footer.jsp" %>

    footer.jsp的内容会被插入到 hello.jsp 转译 成的hello_jsp.java中,最后只会生成一个hello_jsp.java文件

    如果是动作include

    <jsp:include page="footer.jsp" />

    footer.jsp的内容不会被插入到 hello.jsp 转译 成的hello_jsp.java中,还会有一个footer_jsp.java独立存在。 hello_jsp.java 会在服务端访问footer_jsp.java,然后把返回的结果,嵌入到响应中。

    作用?

    因为指令<%@include 会导致两个jsp合并成为同一个java文件,所以就不存在传参的问题,在发出hello.jsp 里定义的变量,直接可以在footer.jsp中访问。

    而动作<jsp:include />其实是对footer.jsp进行了一次独立的访问,那么就有传参的需要。

    7.6 jsp的cookie使用

    有的网站,登陆的时候,会出现一个选项,问你是否要一周内或者一个月内保持登陆状态。 如果你选了,那么一周之内,都不需要再输入账号密码。 这个功能,就是靠cookie来实现的

    那cookie到底是什么呢?

    Cookie是一种浏览器和服务器交互数据的方式。 Cookie是由服务器端创建,但是不会保存在服务器。 创建好之后,发送给浏览器。浏览器保存在用户本地。 下一次访问网站的时候,就会把该Cookie发送给服务器。

    简单通过jsp来演示cookie的功能 提供一个setCookie.jsp用来设置cookie 当然 这个功能也可以在Servlet中定义并使用 这里只是为了简单 使用jsp进行演示

    <%
    
        Cookie c = new Cookie("boxer", "hwj");
        c.setMaxAge(60 * 24 * 60);
        c.setPath("/");
        response.addCookie(c);
        
        <a href="getCookie.jsp">跳转到获取cookie的页面</a>
    %>
    Cookie c = new Cookie("boxer", "hwj");

    表示定义一个cookie的对象 以键值对的方式存储信息 key为 boxer value为 hwj 后续可以通过key获取对应的value值

    c.setMaxAge(60 * 24 * 60);

    表示这个cookie可以保留一天,如果是0,表示浏览器一关闭就销毁

    c.setPath("/");

    Path表示访问服务器的所有应用都会提交这个cookie到服务端, 如果其值是 /a, 那么就表示仅仅访问 /a 路径的时候才会提交 cookie

    response.addCookie(c);

    通过response把这个cookie保存在浏览器端

    访问地址:
    
    http://127.0.0.1/setCookie.jsp

    定义getCookie.jsp用来获取刚才在所设置的cookie属性值

    <%
        Cookie[] cookies = request.getCookies();
        if (null != cookies)
            for (int d = 0; d <= cookies.length - 1; d++) {
                out.print(cookies[d].getName() + ":" + cookies[d].getValue() + "<br>");
            }
    %>

    可以看到我们自己设置的boxer:hwj 显示在页面中

    在浏览器的调试界面中 还会看到一个叫做jsessionid的东西,这是tomcat设置的cookie, 这是tomcat自己定义的cookie,和我们没有关系,会在后续章节中的session中使用到

    cookie原理图

    cookie原理图

    7.7 session的使用

    首先提供一个 用来设置session的jsp setSession.jsp session对象保存数据的方式,有点像Map的键值对(key-value)

    <%
       session.setAttribute("zhanan", "qyh");
    %>
     
    <a href="getSession.jsp">跳转到获取session的页面</a>

    接下来准备用来获取session的jsp getSession.jsp

    <%
        String name = (String)session.getAttribute("zhanan");
    %>
     
    session中的name: <%=name%>

    那session和cookie有什么关系呢? 秦先生和胡伟杰在一起洗完澡之后,都要找到自己的储物柜穿上衣服, 但是,如何找到自己的柜子呢?通过钥匙

    柜子对应服务器中的session 钥匙对应浏览器中的cookie

    session原理图示意

    session原理

    session的有效期

    比如登录一个网站,登录后,在短时间内,依然可以继续访问而不用重新登录。

    但是较长时间不登录,依然会要求重新登录,这是因为服务端的session在一段时间不使用后,就失效了。

    这个时间,在Tomcat默认配置下,是30分钟。

    可以在tomcat的web.xml中通过session-config标签的配置进行修改

    7.8 jsp作用域

    JSP有4个作用域,分别是

    pageContext 当前页面

    requestContext 一次请求

    sessionContext 当前会话

    applicationContext 全局,所有用户共享

    7.8.1 pageContext 当前页面

    pageContext表示当前页面作用域

    准备setContext.jsp和getContext.jsp,分别表示向作用域设置数据,和从作用域获取数据。

    setContext.jsp

    <%
        pageContext.setAttribute("boxer","hwj");
    %>
     
    <%=pageContext.getAttribute("boxer")%>

    getContext.jsp

    <%=pageContext.getAttribute("boxer")%>

    7.8.2 requestContext

    requestContext 表示一次请求。随着本次请求结束,其中的数据也就被回收。

    修改setContext.jsp

    <%
        request.setAttribute("boxer","hwj");
    %>
      
    <%=request.getAttribute("boxer")%>

    修改getContext.jsp

    <%=request.getAttribute("boxer")%>

    request代表的是一次请求,恰好我们的转发也是一次请求 后续我们可以通过转发 进行页面之间的数据传递

    7.8.3 sessionContext

    sessionContext 指的是会话,从一个用户打开网站的那一刻起,无论访问了多少网页,链接都属于同一个会话,直到浏览器关闭。

    所以页面间传递数据,也是可以通过session传递的。

    但是,不同用户对应的session是不一样的,所以session无法在不同的用户之间共享数据。

    修改setContext.jsp

     <%
        session.setAttribute("boxer","hwj");
        response.sendRedirect("getContext.jsp");
    %>
      

    修改getContext.jsp

    <%=session.getAttribute("boxer")%>

    7.8.4 applicationContext

    applicationContext 指的是全局,所有用户共享同一个数据

    修改setContext.jsp

     <%
        application.setAttribute("boxer","hwj");
     
        response.sendRedirect("getContext.jsp");
    %>
      

    修改getContext.jsp

    <%=application.getAttribute("boxer")%>

    7.9 jsp九大内置对象

    jSP的内置对象指的是不需要显示定义,直接就可以使用的对象,比如request,response

    JSP一共有9个隐式对象,分别是

    request

    response

    out

    pageContext

    session

    application

    page

    config

    exception

    request代表请求 response代表相应 out用于输出(一般不用)

    pageContext 代表当前页面作用域

    session 代表当会话作用域

    application 代表当全局作用域

    page 对象即表示当前对象 JSP 会被编译为一个Servlet类 ,运行的时候是一个Servlet实例。 page即代表this

    config可以获取一些在web.xml中初始化的参数。一般也不用 了解即可

    exception 用来做jsp页面的异常处理 了解即可

    7.10 jstl

    7.10.1 常用标签库

    主用core

    maven依赖配置

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    </dependency>
    
    <dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
    </dependency>

    7.10.2 常用标签

    7.10.2.1 <c:set>

    <c:set var="age" value="${20}" scope="request"/>

    7.10.2.2 <c:if>

    <c:if test="${age>18}" >
        <p>这个人成年了</p>
    </c:if>
    <c:if test="${!(age>18)}">
        <p>这个人还没成年</p>

    每一个c:if标签都会根据test后面的boolean值 决定被c:if包括的标签 是否显示

    对象为空的判断

    <c:if test="${empty age}">
        <p>age为空</p>
    </c:if>

    7.11 EL表达式

    典型的模块化标签

    Vue标签与el表达式类似

    区别在于:Vue从js中实例的data中获取数据

    而el表达式 是从当前jsp中的不同作用域中进行数据的获取

    为了能够保证el表达式的使用 需要在上方的page配置中

    添加isElIgnored="false"

    7.11.1 获取值

    <c:set var="zhanan" value="bob" scope="request">

    传统方式获取

     <%=request.getAttribute("zhanan")%>

    el表达式方式获取

    ${zhanan}

    如果给当前页面设置或者传递了任意四个作用域对象之一的属性

    pageContext request session application

    通过${属性的key} 直接获取显示在页面上

    el表达式可以从四个不同的作用域中获取值 但是如果四个作用域中都有个相同属性时怎么获取?

    作用域越小 优先级越高

    优先级:pageContext >request >session>application

    eg:

    <c:set var="zhanan" value="${'bob'}" scope="page">
    <c:set var="zhanan" value="${'alan'}" scope="request">
    <c:set var="zhanan" value="${'alen'}" scope="session">
    <c:set var="zhanan" value="${'jone'}" scope="application">
    
    ${zhanan}//优先获取page的zhanan值   即为bob

    7.11.2 el对JavaBean的操作

    什么是javabean?

    1. 提供无参public的构造方法(默认提供)
    2. 每个属性,都有public的getter和setter
    3. 如果属性是boolean,那么就对应is和setter方法

    访问普通属性 会自动调用getXX方法 访问Boolean属性 会自动调用isXX方法

    学生姓名:${s.name}<br>
    学生年龄:${s.age}<br>
    <c:if test="${s.jerk}">

    7.11.3 <c:if>

    没有c:else

    <c:if test="${s.jerk}">
        是
    </c:if>
    <c:if test="${!s.jerk}">
        否
    </c:if>

    eg:

    StudentListServlet

    public class StudentListServlet extends HttpServlet {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            List<Student> stus  = new ArrayList();
            for (int i = 0; i < 10; i++) {
                Student s = new Student();
                s.setName("渣男"+i);
                s.setAge(18);
                s.setJerk(true);
                stus.add(s);
            }
    //        集合存入request中
            req.setAttribute("stus",stus);
    //        通过转发将数据传递给test1.jsp
            req.getRequestDispatcher("test1.jsp").forward(req,resp);
        }
    }

    html body部分

    <table width="500px" align="center" border="1px solid black" cellspacing="0">
        <tr>
            <td>学生编号</td>
            <td>姓名</td>
            <td>年龄</td>
        </tr>
    <%--    <c:forEach></c:forEach>--%>
        <c:forEach items="${stus}" var="stu" varStatus="st">
            <tr>
                <td>${st.count}</td>
                <td>${stu.name}</td>
                <td>${stu.age}</td>
            </tr>
        </c:forEach>
    </table>

    7.11.4 c:forEach

    items 代表session的key var表示取出的值 varStatus必须加 相当于Vue 中 v-for的key

    <c:forEach items="${stus}" var="stu" varStatus="st">
        <tr>
            <td>${st.count}</td>
            <td>${stu.name}</td>
            <td>${stu.age}</td>
        </tr>
    </c:forEach>

    7.11.3 MVC架构

    M model javabean DAO

    V View (jsp)

    C Controller(Servlet) 根据接收请求的不同 获取不同的数据 且负责将数据传递给不同的页面

    eg

    <table align="center" border="1px solid black" cellspacing="0">
        <tr>
            <td>id</td>
            <td>name</td>
            <td>price</td>
            <td>stock</td>
            <td>增加</td>
            <td>删除</td>
        </tr>
        <c:forEach items="${products}" var="p" varStatus="st">
            <tr>
                <td>${p.id}</td>
                <td>${p.name}</td>
                <td>${p.price}</td>
                <td>${p.stock}</td>
    <%--            相对路径--%>
    <%-- ?之前的表示需要完成的action或者跳转的响应或者页面 ?后表示的时候页面跳转所携带的数据 --%>
                <td><a href="editProduct?id=${p.id}">edit</a></td>
                <td><a href="deleteProduct?id=${p.id}">delete</a></td>
            </tr>
        </c:forEach>
    </table>
    <br>
    <%--提供要新增的数据--%>
    <form action="addproduct">
    name:<input type="text" name="name"><br>
    price:<input type="text" name="price"><br>
    stock:<input type="text" name="stock"><br>
    <input type="submit" value="提交">

    点击edit连接后 完成 ProductEditServlet

    public class ProductEditServlet extends HttpServlet {
    
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            ProductDAO productDAO = new ProductDaoImpl();
    //        根据edit超链接获取对应的参数
            int id = Integer.parseInt(req.getParameter("id"));
    //        根据id获取对应的product对象 用来传递给editProduct.jsp页面
            Product p = productDAO.get(id);
    //        将对象存人到req中
            req.setAttribute("p",p);
    //        跳转到编辑商品的界面
            req.getRequestDispatcher("editProduct.jsp").forward(req,resp);
        }
    }

    web.xml中相关配置为

    <servlet>
      <servlet-name>ProductEditServlet</servlet-name>
      <servlet-class>com.individual.servlet.ProductEditServlet</servlet-class>
    </servlet>
    <servlet-mapping>
      <servlet-name>ProductEditServlet</servlet-name>
      <url-pattern>/editProduct</url-pattern>
    </servlet-mapping>

    完成之后 携带product数据 跳转至editProduct.jsp

    <form action="updateproduct?id=${p.id}">
        name:<input type="text" name="name" value="${p.name}"><br>
        price:<input type="text" name="price" value="${p.price}"><br>
        stock:<input type="text" name="stock" value="${p.stock}"><br>
        <input type="submit" value="提交">
    </form>

    7.12 过滤器

    7.12.1 过滤器基本介绍

    在用户的请求访问由tomcat交由对应的Servlet之前,请求必须通过所有的过滤器,满足过滤器的放行条件,才可以最终访问到对应的Servlet,否则请求就会被拦截。

    7.12.2 过滤器简单应用

    //所有请求都会被拦截
    @WebFilter(urlPatterns = {"/*"})
    //拦截所有对html页面访问的请求
    //@WebFilter(urlPatterns = {"/*.html"})

    实现Filter接口

    编写第一个filter

    public class FirstFilter implements Filter {
     
        @Override
        public void destroy() {
     
        }
     
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
     
            String ip = request.getRemoteAddr();
            String url = request.getRequestURL().toString();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date d = new Date();
            String date = sdf.format(d);
     
            System.out.printf("%s %s 访问了 %s%n", date, ip, url);
            chain.doFilter(request, response);
        }
     
        @Override
        public void init(FilterConfig arg0) throws ServletException {
     
        }
     
    }

    不使用注解时,web.xml的配置如下

    <filter>
        <filter-name>FirstFilter</filter-name>
        <filter-class>filter.FirstFilter</filter-class>
        </filter>
     
        <filter-mapping>
        <filter-name>FirstFilter</filter-name>
        <url-pattern>/*</url-pattern>
        </filter-mapping>

    注解

    过滤器执行顺序之和过滤器类的首字母有关 和其它配置信息无关 filtername无关

    @WebFilter(urlPatterns = {"/*"}, filterName = "B")

    如果希望多个url匹配同一个Servlet 增加urlpatterns

    @WebFilter(urlPattrens={"/login","/xx})

    7.12.3 Filter 的init()

    可以在重写init方法来定义你需要在过滤器启动的时候做什么 Filter一定会随着tomcat的启动自启动。 Filter是web应用非常重要的一个环节,如果Filter启动失败,或者本身有编译错误,不仅这个Filter不能使用,整个web应用会启动失败,导致用户无法访问页面

    7.12.4 doFilter()

    doFilter()方法中 参数的类型 和我们常用的请求和响应的参数不一致 为了方便调用方法

    需要先对请求和响应参数做参数转型

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;

    获取请求的ip地址和url

    String ip = request.getRemoteAddr();
    String url = request.getRequestURI().toString();

    加一个时间

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String date = sdf.format(new Date());

    当前过滤器对请求进行放行,如果有下一个过滤器,则将请求交给下一个过滤器//如果没有则让请求正常访问servlet

    此句代表过滤器放行

    chain.doFilter(request, response);

    7.12.5 Filter 中文处理

    在之前的学习中我们知道,可以在需要做请求中文处理的地方,使用

    request.setCharacterEncoding("UTF-8");

    但是如果我们当前项目中Servlet非常多,每一个Servlet中都进行中文配置,代码就会显得非常的繁琐,所以我们可以使用过滤器来进行统一中文处理

    提供Encodingfileter

    public class EncodingFilter implements Filter {
     
        @Override
        public void destroy() {
     
        }
     
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            //Filter中参数类型需要强转之后才可以使用    
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
     
            request.setCharacterEncoding("UTF-8");
     
            chain.doFilter(request, response);
        }
     
        @Override
        public void init(FilterConfig arg0) throws ServletException {
     
        }
     
    }

    7.12.6 Filter做登录验证

    之前我们结合Session判断过用户是否登录,但是同样的道理,如果每一次Session的判断都需要定义在Servlet中,则书写非常不方便,可以通过过滤器进行完成

    创建一个AutherFilter类,对用户的登录进行验证

    public class AuthFilter implements Filter {
     
        @Override
        public void destroy() {
     
        }
     
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
     //获取uri
            String uri = request.getRequestURI();
            //判断请求是否是登录界面,或者访问的uri是login,如果是,
            //则请求放行,否则会进入到判断逻辑的死循环,无法运行后续代码
            if (uri.endsWith("login.html") || uri.endsWith("login")) {
                chain.doFilter(request, response);
                return;
            }
     
            String userName = (String) request.getSession().getAttribute("userName");
            if (null == userName) {
                response.sendRedirect("login.html");
                return;
            }
     
            chain.doFilter(request, response);
        }
     
        @Override
        public void init(FilterConfig arg0) throws ServletException {
     
        }
     
    }

    7.13 监听器

    负责监听web应用的创建和销毁 以及其上属性的变化

    而web应用对应的是jsp的隐式对象application

    还可以监听session和request的生命周期 以及他们的属性变化

    7.13.1 SessionListener

    @WebListener
    public class SessionListener implements HttpSessionListener {
        @Override
        public void sessionCreated(HttpSessionEvent httpSessionEvent) {
            System.out.println("session被创建了");
    //        监听session的创建
    //        System.out.println(httpSessionEvent.getSession().getId());
    
        }
    
        @Override
        public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
    //        监听到session被销毁了
            System.out.println("session被销毁了");
    
        }
    }

    7.13.2 RequestListener

    可以实现以下接口

    ServletRequestListener, ServletRequestAttributeListener

    全局的使用

    ServletContext application =  httpSessionEvent.getSession().getServletContext();

    7.14 注解

    基于注释和代码

    既可以起到注释的作用

    也可以起到代码的作用 代替部分xml配置文件的作用

    7.12.1 内置注解

    @Override

    表示该方法重写了父类

    如果被该注解所注释的方法,并没有对父类方法进行重写 编译无法通过

    public class Student {
        private String name;
        @Override
        public String toString(){
            return name;
        }
    
    }

    @Deprecated

    表示该方法在后续版本中可能弃用,尽量避免使用

    @SuppressWarnings

    警告抑制

    @SuppressWarnings(“参数”)

    • deprecation 使用@Deprecated产生的警告,可以使用该注解抑制警告
    • unchecked 当集合没有使用泛型的时候 一直这一类警告
    • fallthrough 当switch程序块直接通往下一种情况 而没有break时的警告
    • path 在类路径 源文件路径中 等 有不存在的路径时候的警告抑制
    • serial 当可序列化的类 缺少serialVersionUID时候的警告抑制
    • finally 当任何finally自己不能正常完成的时候的警告抑制
    • rawtypes 泛型类型未指明时的警告抑制
    • UNUSED 引用定义了 但没有被使用的警告抑制
    • all 关于以上所有情况的警告抑制

    7.12.2 元注解

    用来注解自定义注解

    用来修饰自定义注解的属性 适用范围等

    @Target

    表示当前定义的自定义注解 可以用在哪些位置上

    • TYPE 表示修饰类或者接口
    • FIELD 能够修饰成员变量
    • METHOD 能够修饰方法
    • PARAMETER 能够修饰参数
    • CONSTRUCTOR 能够修饰构造函数
    • VARIABLE 能够修饰局部变量
    • PACKAGE 能够修饰包

    @Retention

    定义了注解的生命周期

    • RetentionPolicy.source 当前注解只在源代码中保存 编译成class之后 就失效了 例如@Override
    • RetentionPolicy.class 注解在java文件编译成.class文件后 依旧存在 但是运行之后就消失了 也是@Retention的默认值
    • RetentionPolicy.RUNTIME 注解在运行起来之后 依旧存在 程序可以通过反射获取这些信息

    @Inherited

    表示当前注解具有继承性 子类可以直接获取注解的属性

    @Repeatable

    当没有该注解修饰的时候 注解在同一个位置 只能出现一次

    7.12.3 自定义注解

    结合自定义注解方式来改造我们自己所写的jdbcutil类的代码

    这是我们传统的写法

    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public class JDBCUtils {
        static String ip = "127.0.0.1";
        static int port = 3306;
        static String database = "test";
        static String encoding = "UTF-8";
        static String loginName = "root";
        static String password = "admin";
        static{
            try {
                Class.forName("com.mysql.jdbc.Driver");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        public static Connection getConnection() throws SQLException {
            String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
            return DriverManager.getConnection(url, loginName, password);
        }
        public static void main(String[] args) throws SQLException {
            System.out.println(getConnection());
        }
    }

    接下来 我们借助注解进行参数传递

    提供自定义注解@JDBCConfig

    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.TYPE;
     
    import java.lang.annotation.Documented;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
     
    @Target({METHOD,TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface JDBCConfig {
         String ip();
         int port() default 3306;
         String database();
         String encoding();
         String loginName();
         String password();
    }

    自定义注解中使用到了部分元注解,元注解一会会详细讲解

    @Target({METHOD,TYPE})

    表示这个注解可以用用在类/接口上,还可以用在方法上

    @Retention(RetentionPolicy.RUNTIME)

    表示这是一个运行时注解,即运行起来之后,才获取注解中的相关信息,而不像基本注解如@Override 那种不用运行,在编译时eclipse就可以进行相关工作的编译时注解。

    @Inherited

    表示这个注解可以被子类继承

    @Documented

    表示当执行javadoc的时候,本注解会生成相关文档.

    注解方式完成JDBCUtil的配置

    @JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
    public class DBUtil {
        static {
            try {
                Class.forName("com.mysql.jdbc.Driver");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
            public static Connection getConnection() throws SQLException, NoSuchMethodException, SecurityException {
            JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);
     
            String ip = config.ip();
            int port = config.port();
            String database = config.database();
            String encoding = config.encoding();
            String loginName = config.loginName();
            String password = config.password();
     
            String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
            return DriverManager.getConnection(url, loginName, password);
        }
         
        public static void main(String[] args) throws NoSuchMethodException, SecurityException, SQLException {
            Connection c = getConnection();
            System.out.println(c);
        }
     
    }

    7.12.4 Servlet常用注解

    代替繁重的xml配置

    xml和注解只能使用一种

    @WebServlet

    • asyncsupported声明servlet是否支持异步操作模式
    • description关于servlet的描述
    • displayName servlet的显示名称
    • initParams servlet的init参数
    • name servlet名称
    • urlpatterns 用来定义访问Servlet的url
    • value 作用相同

    @webservlet (name="Helloservlet , value=" / hello" )

    • 定义多个url访问同一个servlet
      @WebServlet (name="ProductListServlet", urlPatterns={ "/list" " /listProduct" ))

    @WebFilter

    @WebListener

    7.15 ajax

    7.15.1 ajax基本介绍

    AJAX 不是新的编程语言,而是一种使用现有标准的新方法。

    (本质上可以理解为是一种 通过js和后端进行交互的一种方式)

    AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。

    通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。

    这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

    传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。

    有很多使用 AJAX 的应用程序案例:新浪微博、Google 地图、开心网等等。 AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。

    7.15.2 ajax基本原理

    ajax基本原理

    7.15.3 前后端分离和前后端不分离的区别

    前后端不分离

    浏览器----------------------------------------->tomcat服务器

    1.服务器不运行或者jsp文件中有编译错误页面就无法查看
    ⒉.前端页面的内容和后端的功能代码必须由同一个人完成开发效率比较低下
    3.jsp页面每次内容更新必须从服务器获取整个页面的内容
    这就是前后端不分离带来的问题

    tomcat------------------------------------------>浏览器

    访问ListProductServlet
    该Servlet中通过DAO层的调用获取相关数据并通过转发将数据提供给listProduct.jsp页面

    jsp页面通过el表达式解析数据解析完成之后先转译成servlet在进行编译最终将页面的所有数据以字符串的方式写入到响应中伴随着响应返回给浏览器

    整个页面的数据被jsp对应的转译Servlet通过流和响应以字符串的方式返回给浏览器页面浏览器再根据http协议对响应的内容进行解析

    前后端分离

    前后端分离

    8 Junit

    单元测试工具

        @Test
        public void testSum(){
            int result = SumUtil.sum(1,2);
    //        result是运行的数据   actual是期望值
            Assert.assertEquals(result,3);
        }
    //    两个其它常用注解
    //    测试方法之前被执行
    //@Before       测试之前的准备工作   比如驱动加载数据库连接等
    //@After        测试完成之后的工作,如流的关闭或资源的释放
    //@BeforeClass  被该注解修饰的方法,必须是静态方法  并且在整个测试环节中只会被执行一次除此之外 和before功能相同
    //@AfterClass   被该注解修饰的方法,必须是静态方法  并且在整个测试环节中只会被执行一次除此之外 和after功能相同

    TestSuite

    @RunWith(Suite.class)
    //    告诉suit需要同时执行和测试的测试类和类对象
    @Suite.SuiteClasses({TestCase1.class,TestCase2.class})
    public class TestSuite {
    
    }

    9 Spring

    9.1 Spring基本介绍

    一个轻量级的开源JAVAEE框架

    解决企业应用开发的复杂性

    对象管理

    9.2 核心技术

    ioc、di 、aop

    9.2.1 ioc

    Inversion of Control 控制反转 将创建对象的权利由程序员 交给spring

    面向编程中的一种设计原则 可以用来降低代码之间的耦合度

    基于IOC容器实现的 IOC容器底层使用了对象工厂概念

    IOC容器实现有两种方式

    • BeanFactory 是ioc容器的基本实现 是spring内部使用的接口 开发人员无法直接使用

    该容器在加载配置文件的时候不会创建对象 在获取和使用对象的时候 才会创建对象 (懒汉模式)

    • ApplicationContext 是BeanFactory的子接口 提供了更多功能 一般由开发人员使用

    该容器在加载配置文件的时候 就会创建配置的对象 (饿汉模式)

    • 优点:

      • 1、低侵入式设计:非入侵式设计,基于Spring开发的应用一般不依赖于Spring的类
      • 2、独立于各种应用服务器,真正实现:一次编写,到处运行。
      • 3、Spring的依赖注入特性使Bean与Bean之间的依赖关系变的完全透明,降低了耦合度:使用IOC容器,将对象之间的依赖关系交给Spring,降低组件之间的耦合性,让我们更专注于应用逻辑
      • 4、面向切面编程特性允许将一些通用任务如安全、事务、日志等进行集中式处理
      • 5、提供了与第三方持久层框架的良好整合,并简化了底层数据库访问
      • 6、高度的开放性(可以和Struts2、Hibernate、MyBatis、CXF等很多主流第三方框架无缝整合)

    9.2.2 di

    Dependency Injection 依赖注入 所有对象的属性赋值 由程序员转交给spring

    9.2.3 aop

    Aspect Oriented Proramming 面向切面编程

    9.3 如何使用

    依赖包:

    • spring-beans-5.3.2.jar
    • spring-context-5.3.2.jar
    • spring-core-5.3.2.jar
    • spring.expression.jar

    定义普通类

    public class Person {
        private String[] catNameArray;
        private List<String> catNameList;
        private Map<String,String> catNameMap;
        private Set<String> catNameSet;
    
        public String[] getCatNameArray() {
            return catNameArray;
        }
    
        public void setCatNameArray(String[] catNameArray) {
            this.catNameArray = catNameArray;
        }
    
        public List<String> getCatNameList() {
            return catNameList;
        }
    
        public void setCatNameList(List<String> catNameList) {
            this.catNameList = catNameList;
        }
    
        public Map<String, String> getCatNameMap() {
            return catNameMap;
        }
    
        public void setCatNameMap(Map<String, String> catNameMap) {
            this.catNameMap = catNameMap;
        }
    
        public Set<String> getCatNameSet() {
            return catNameSet;
        }
    
        public void setCatNameSet(Set<String> catNameSet) {
            this.catNameSet = catNameSet;
        }
    
        public Person() {
            System.out.println("我  被创建了");
        }
    
        private Cat cat;
        public Cat getCat(){
            return cat;
        }
        public void setCat(Cat cat){
            this.cat = cat;
        }
        public void luCat(){
            System.out.println("写代码撸猫");
        }
        public void play(){
            System.out.println("就是玩");
        }
    
    }
    public class Cat {
        String name;
        String type;
        int age;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public Cat() {
        }
    
        //    提供一个有参构造
    //    用于构造注入
        public Cat(String name) {
            this.name = name;
        }
    
        public String getType() {
            return type;
        }
    //   设值注入
        public void setType(String type) {
            this.type = type;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public void miao(){
            System.out.println("miao");
        }
        public void eat(){
            System.out.println("eat");
        }
    
        @Override
        public String toString() {
            return "Cat{" +
                    "name='" + name + '\'' +
                    ", type='" + type + '\'' +
                    '}';
        }
    }

    在resource文件夹下创建spring配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--配置一个创建Person对象的bean-->
        <bean id="person" class="com.iweb.spring5.Person"></bean>
    </beans>

    编写代码测试

    public class TestSpring5 {
        @Test
        public void testPserson(){
            // 1.加载配置文件
            ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
            // 2.获取配置文件中的对象
            Person person = context.getBean("person", Person.class);
            person.speak();
        }
    }

    报错

    java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

    下载并导入日志的依赖包commons-logging, 然后再次运行

    9.4 IOC/DI控制反转 / 依赖注入

    • 控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。
    • 其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)
    • 在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但是在spring里,创建被调用者的工作不再由调用者来完成。创建被调用者实例的工作通常由spring容器来完成,然后注入给调用者,称为控制反转,也称为依赖注入。这样给程序带来很大的灵活性,这样也实现了我们的接口和实现的分离。

      • 将组件对象的控制权从代码本身转移到外部容器
      • 组件化的思想:分离关注点,使用接口,不再关注实现

    IOC底层原理:

    • xml解析
    • 简单工厂
    • 反射

    9.5 Spring的Bean管理

    Spring的Bean管理,主要管理bean的两种操作

    • bean的创建
    • 属性值的注入

    操作方式:

    • 基于xml配置方式
    • 基于注解操作方式

    9.5.1 基于xml配置方式创建bean

    <bean id="person" class="com.iweb.spring5.Person"></bean>

    id属性:对象的唯一标示,通过标示获取对象的实例

    class属性:对象所对应的类的完全限定名

    scope属性:取值prototype/singleton(默认)

    使用上述方式创建对象时,要保证类中有无参构造方法。

    9.5.2 基于xml配置方式注入属性值

    • DI,依赖注入,即 注入属性,主要分为设值注入与构造注入。

    9.5.2.1 设值注入

    <bean id="cat" class="com.iweb.spring5.Cat">
        <property name="name" value="tom"/>
        <property name="type" value="加菲"/>
    </bean>

    9.5.2.2 构造注入

    <bean id="cat" class="com.iweb.spring5.Cat">
        <constructor-arg name="name" value="tom" />
    </bean>

    9.5.2.3 p名称空间注入(简化写法)

    1. 添加p名称空间的约束

      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:p="http://www.springframework.org/schema/p"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    2. 在bean中直接使用p属性

      <bean id="cat" class="com.iweb.spring5.Cat" p:name="tom" p:type="加菲"></bean>

    9.5.2.4 注入空值

    <bean id="cat" class="com.iweb.spring5.Cat">
        <property name="name">
            <null/>
        </property>
    </bean>

    9.5.2.5 注入特殊符号

    <bean id="cat" class="com.iweb.spring5.Cat">
        <property name="name">
            <value><![CDATA[>-_-<]]></value>
        </property>
    </bean>
    Cat{name='>-_-<', type='null'}

    9.5.2.6 注入外部bean

    <bean id="person" class="com.iweb.spring5.Person">
        <property name="cat" ref="cat"/>
    </bean>
    <bean id="cat" class="com.iweb.spring5.Cat">
        <property name="name">
            <value><![CDATA[>-_-<]]></value>
        </property>
    </bean>
    @Test
    public void testPserson(){
        // 1.加载配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
        // 2.获取配置文件中的对象
        Person person = context.getBean("person", Person.class);
        person.luCat();
    }

    运行结果:

    终于下班到家了,好累,撸撸猫减压。。
    >-_-<:咕噜咕噜。。。。。

    9.5.2.7 注入内部bean

    内部bean只在某个bean的内部可以访问,外部的其他bean无法访问它。

    <bean id="person" class="com.iweb.spring5.Person">
        <property name="cat">
            <bean class="com.iweb.spring5.Cat">
                <property name="name">
                    <value><![CDATA[>-_-<]]></value>
                </property>
            </bean>
        </property>
    </bean>
    @Test
    /**
      * 测试注入内部bean
      */
    public void testPserson1(){
        // 1.加载配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
        // 2.获取配置文件中的对象
        Person person = context.getBean("person", Person.class);
        person.luCat();
    }

    9.5.2.8 级联赋值

    <bean id="person" class="com.iweb.spring5.Person">
        <property name="cat" ref="cat"/>
        <property name="cat.type" value="波斯猫"/>
    </bean>
    <bean id="cat" class="com.iweb.spring5.Cat">
        <property name="name">
            <value><![CDATA[>-_-<]]></value>
        </property>
    </bean>

    PS: 级联赋值要保证对应的属性有setter

    9.5.2.9 注入集合

    // Person类的部分属性
    private String[] catNameArray;
    private List<String> catNameList;
    private Map<String, String> catNameMap;
    private Set<String> catNameSet;
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--   级联赋值-->
        <bean id="person" class="com.iweb.spring5.Person">
            <property name="cat" ref="cat"/>
            <property name="cat.type" value="波斯猫"/>
            <!--        注入数组-->
            <property name="catNameArray">
                <array>
                    <value>tom</value>
                    <value>jerry</value>
                    <value>kitty</value>
                </array>
            </property>
            <!--        注入List-->
            <property name="catNameList">
                <list>
                    <value>tom</value>
                    <value>jerry</value>
                    <value>kitty</value>
                </list>
            </property>
            <!--        注入map-->
            <property name="catNameMap">
                <map>
                    <entry key="tom" value="汤姆"/>
                    <entry key="jerry" value="杰瑞"/>
                    <entry key="kitty" value="凯蒂"/>
                </map>
            </property>
            <property name="catNameSet">
                <set>
                    <value>tom</value>
                    <value>jerry</value>
                    <value>kitty</value>
                </set>
            </property>
        </bean>
        <bean id="cat" class="com.iweb.spring5.Cat">
            <property name="name">
                <value><![CDATA[>-_-<]]></value>
            </property>
            <property name="age" value="2"/>
        </bean>
    </beans>

    9.5.2.10 注入外部集合

    1. 添加util名称空间

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:util="http://www.springframework.org/schema/util"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
    2. 配置并注入外部集合

      <!--注入外部集合-->
      <bean id="person" class="com.iweb.spring5.Person">
          <property name="catNameList" ref="catNameList"/>
      </bean>
      <!--配置独立外部集合-->
      <util:list id="catNameList">
          <value>tom</value>
          <value>jerry</value>
          <value>kitty</value>
      </util:list>

    9.6.FactoryBean(了解)

    在Spring中,管理着两种Bean

    • 普通Bean: 在配置文件中定义的类型与返回类型保持一致
    • FactoryBean: 在配置文件中定义的类型与返回类型可以不一致

    FactoryBean的实现步骤:

    1. 创建类,实现FactoryBean接口
    2. 实现接口方法,并在相关方法中定义返回的bean的类型

    9.7.Bean的作用域

    在Spring容器中,可以通过scope设置Bean的作用域, 取值有:

    • prototype 多实例
    • singleton 单实例(默认)

    单实例演示:

    <bean id="cat" class="com.iweb.spring5.Cat"></bean>
    Cat cat1 = context.getBean("myBean", Cat.class);
    Cat cat2 = context.getBean("myBean", Cat.class);
    System.out.println(cat1);
    System.out.println(cat2);

    运行结果:

    com.iweb.spring5.Cat@b9afc07
    com.iweb.spring5.Cat@b9afc07

    多实例演示:

    <bean id="cat" scope="prototype" class="com.iweb.spring5.Cat"></bean>

    运行结果:

    com.iweb.spring5.Cat@382db087
    com.iweb.spring5.Cat@73d4cc9e

    9.7.1单例模式的实现

    单例模式:系统运行期间,有且仅有一个实例

    • 一个类只有一个实例——最基本的原则

      • 只提供私有构造器
    • 它必须自行创建这个实例

      • 定义了静态的该类私有对象
    • 它必须自行向整个系统提供这个实例

      • 提供一个静态的公有方法,返回创建或者获取本身的静态私有对象

    单例模式示例代码:

    public class Server {
        // 静态私有对象
        private static Server server;
        // 私有构造方法
        private Server() {}
        // 静态公有方法,用于获取对象
        public static Server getInstance() {
            if (server == null) {
                server = new Server();
            }
            return server;
        }
    }

    PS: 在并发环境下,上述的单例模式实现是否存在弊端,线程是否安全?是否会出现多个Server的实例?

    如何解决?

    常用解决方法:

    • 懒汉模式
    • 饿汉模式
    • 静态内部类

    9.7.1.1 懒汉模式

    在类加载时不创建实例,采用延迟加载的方式,在运行调用时创建实例。

    特点:

    • 线程不安全
    • 延迟加载(lazy Loading)

    如何解决线程安全问题?使用同步synchronized

    public class Server {
        // 静态私有对象
        private static Server server;
        // 私有构造方法
        private Server() {}
        // 使用同步解决线程安全问题
        public synchronized static Server getInstance() {
            if (server == null) {
                server = new Server();
            }
            return server;
        }
    }

    9.7.1.2 饿汉模式

    在类加载的时候,就完成初始化

    特点:

    • 线程安全
    • 不具备延迟加载特性
    public class Server1 {
        private static Server1 server = new Server1(); 
        private Server1() {}   
        public static Server1 getInstance(){
            return server;
        }  
    }

    9.7.1.3 静态内部类

    若实例化单例类很消耗资源,那么我们就希望它可以延迟加载,即不想让它在类加载时就实例化,那如何处理呢?

    要求饿汉模式同时要具备延迟加载的特性, 使用静态内部类解决。

    public class Server2 {
        private Server2() {}
        private static class ServerHelper{
            private static Server2 INSTANCE=new Server2();
        }
        public static Server2 getInstance(){
            return ServerHelper.INSTANCE;
        }
    }

    9.8 bean的生命周期

    表示从创建到销毁的过程

    9.8.1 常规5步生命周期

    1. 通过构造器创建Bean的实例
    2. 为bean的属性设置值(调用set方法)
    3. 调用bean的初始化的方法(如果有)(需要手动配置对应方法)
    4. 获取到bean的实例
    5. 当容器关闭时,调用bean的销毁方法(如果有)(需要手动配置对应方法)
    public class Dog {
        public Dog() {
            System.out.println("调用构造方法Dog()创建Dog对象。。。");
        }
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            System.out.println("调用setter设置属性值。。。");
            this.name = name;
        }
        /**
         * 测试bean的初始化方法
         */
        public void testFun() {
            System.out.println("调用bean的初始化方法。。。");
        }
        /**
         * 测试bean的销毁方法
         */
        public void destroy(){
            System.out.println("调用bean的销毁方法。。。");
        }
    }
    <bean id="dog" class="com.iweb.spring5.lifecycle.Dog" 
          init-method="testFun" destroy-method="destroy">
        <property name="name" value="tom" />
    </bean>
    @Test
    public void testLifeCycle() {
        ClassPathXmlApplicationContext context = 
            new ClassPathXmlApplicationContext("lifecycle.xml");
        Dog dog = context.getBean("dog", Dog.class);
        System.out.println(dog);
        // 手动关闭容器
        context.close();
    }

    9.8.2 完整7步生命周期

    1. 通过构造器创建Bean的实例
    2. 为bean的属性设置值(调用set方法)
    3. bean初始化前,调用前置处理器postProcessBeforeInitialization()方法
    4. 调用bean的初始化的方法(如果有)(需要手动配置对应方法)
    5. bean初始化后,调用后置处理器postProcessAfterInitialization()方法
    6. 获取到bean的实例
    7. 当容器关闭时,调用bean的销毁方法(如果有)(需要手动配置对应方法)
    8. 自定义后置处理器,并实现后置处理器接口:

      /**
       * @autor g
       * @data 2021/1/14
       * @desc 测试bean的生命周期-自定义后置处理器
       */
      public class MyPostProcessor implements BeanPostProcessor {
          @Override
          public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
              System.out.println("3.0 bean初始化前,调用postProcessBeforeInitialization()方法。。。");
              return null;
          }
          @Override
          public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
              System.out.println("3.1 bean初始化后,调用postProcessAfterInitialization()方法。。。");
              return null;
          }
      }
    9. 在Spring容器中配置自定义的后置处理器

      <bean id="myPostProcessor" 
            class="com.iweb.spring5.lifecycle.MyPostProcessor"></bean>
    10. 测试testLifeCycle() 并观察运行结果:

      1.调用构造方法Dog()创建Dog对象。。。
      2.调用setter设置属性值。。。
      3.0 bean初始化前,调用postProcessBeforeInitialization()方法。。。
      3.调用bean的初始化方法。。。
      3.1 bean初始化后,调用postProcessAfterInitialization()方法。。。
      4.使用beanDog{name='tom'}
      5.调用bean的销毁方法。。。

    9.9 Bean的自动装配

    在Spring容器中,bean的属性的注入,除了可以手动设置,容器也可以自动注入属性值。

    使用方式,设置bean的aotuwire属性值:

    • byName 根据属性名称自动注入(要注入的bean的名称要跟属性名保持一致)
    • byType 根据属性类型自动注入
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--    演示属性的自动装配,根据名称-->
        <bean id="person" class="com.iweb.spring5.Person" autowire="byName"></bean>
        <bean id="cat" class="com.iweb.spring5.Cat"></bean>
    </beans>
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--    演示属性的自动装配, 根据类型-->
        <bean id="person" class="com.iweb.spring5.Person" autowire="byType"></bean>
        <bean id="cat1" class="com.iweb.spring5.Cat"></bean>
    </beans>
    @Test
    public void testXmlAutowire() {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("bean8.xml");
        Person person = context.getBean("person", Person.class);
        System.out.println(person.getCat());
    }

    运行结果:

    com.iweb.spring5.Cat@47db50c5

    9.10 加载外部配置文件

    1. 导入druid的jar包
    2. 在xml文件中配置druid对应的bean

      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
          <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
          <property name="url" value="jdbc:mysql://192.168.200.6:3306/littlemall?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC"></property>
          <property name="username" value="root"></property>
          <property name="password" value="123456"></property>
      </bean>
    3. 使用外部配置文件改进bean的配置

      • 创建db.properties保存连接数据库的配置信息

        jdbc.url=jdbc:mysql://192.168.200.6:3306/littlemall?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC
        jdbc.driver=com.mysql.cj.jdbc.Driver
        jdbc.username=com.mysql.cj.jdbc.Driver
        jdbc.password=123456
      • 在xml文件中配置context上下文名称空间,以加载外部配置文件

        <beans xmlns="http://www.springframework.org/schema/beans"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               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/context http://www.springframework.org/schema/context/spring-context.xsd">
      • 在xml文件中加载外部配置文件

        <context:property-placeholder location="classpath:db.properties" />
      • 使用表达式占位符获取外部配置文件中的值

        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${jdbc.driver}"></property>
            <property name="url" value="${jdbc.url}"></property>
            <property name="username" value="${jdbc.username}"></property>
            <property name="password" value="${jdbc.password}"></property>
        </bean>

    PS: 完整配置文件如下:

    <?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: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/context http://www.springframework.org/schema/context/spring-context.xsd">
        <!--    导入外部配置文件-->
        <context:property-placeholder location="classpath:db.properties" />
        <!--    配置druid连接池-->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${jdbc.driver}"></property>
            <property name="url" value="${jdbc.url}"></property>
            <property name="username" value="${jdbc.username}"></property>
            <property name="password" value="${jdbc.password}"></property>
        </bean>
    </beans>

    9.11 基于注解方式创建Bean

    • 目的:简化配置的操作
    • 定义:是代码的一种特殊标记,语法格式为

      @注解名称(属性名=属性值,属性名=属性值)
    • 使用场景:用于类上、方法上、属性上

    Spring容器对于管理的Bean提供了四种注解, 这四种注解功能一致,都可以创建Bean:

    • @Component 普通注解,可以创建Bean
    • @Service Service注解,一般用于Service层的bean
    • @Controller Controller注解,一般用于控制器层的Bean
    • @Repository Repository注解,一般用于Dao层

    使用步骤:

    1. 导入aop依赖包--spring-aop-5.3.2.jar
    2. 配置xml文件的上下文名称空间context,以便Spring容器扫描要管理的的Bean

      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:context="http://www.springframework.org/schema/context"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
      
      </beans>
    3. 开启组件(包)的扫描,以便Spring容器扫描到管理的的Bean

    PS: 多个包之间用英文逗号隔开,也可以配置扫描父包

    <context:component-scan base-package="com.iweb.spring5.dao,com.iweb.spring5.service"/>
    <!--    配置父包-->
    <context:component-scan base-package="com.iweb.spring5"/>
    1. 在需要被Spring管理的类上配置注解

      @Service(value="userService")
      // 以上等同于 <bean id="userService" class="com.iweb.spring5.service.impl.UserServiceImpl" />
      public class UserServiceImpl implements UserService {
          @Override
          public int add() {
              return 0;
          }
      }
    2. 若希望不扫描某些包,可以进行自定义配置(use-default-filters="false")

    比如, 只扫描@Service注解

    <context:component-scan base-package="com.iweb.spring5"
                            use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
    </context:component-scan>

    比如,排除@Repository注解

    <context:component-scan base-package="com.iweb.spring5">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>

    PS: 不要随便排除@Component注解,一旦排除,所有相关的@Service, @Controller,@Repository都将失效

    ## 9.12 基于注解方式注入属性的值

    • @AutoWired 根据属性类型自动注入(装配)
    • @Qualifier 根据属性名称自动注入
    • @Resource 根据属性名称或类型自动注入
    • @Value 注入普通值

    ### 9.12.1 AutoWired

    根据类型自动注入示例:

    @Service(value="userService")
    // <bean id="userService" class="com.iweb.spring5.service.impl.UserServiceImpl" />
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserDao userDao;
        @Override
        public int add() {
            System.out.println(userDao);
            return 0;
        }
    }
    @Repository
    public class UserDaoImpl implements UserDao {
        @Override
        public int add() {
            return 0;
        }
    }
    @Test
    public void testUserService() {
        ApplicationContext context =
            new ClassPathXmlApplicationContext("bean7.xml");
        UserService userService = context.getBean("userService", UserService.class);
        System.out.println(userService);
        userService.add();
    }

    运行结果:

    com.iweb.spring5.service.impl.UserServiceImpl@358ee631
    com.iweb.spring5.dao.impl.UserDaoImpl@ec756bd

    9.12.2 Qualifier

    当实现类有多个时,可以根据名称指定自动注入哪个子类实例

    @Service(value="userService")
    public class UserServiceImpl implements UserService {
        @AutoWired
        @Qualifier(value="userDao")
        private UserDao userDao;
        @Override
        public int add() {
            System.out.println(userDao);
            return 0;
        }
    }
    @Repository(value = "userDao")  // 指定名称
    public class UserDaoImpl implements UserDao {
        @Override
        public int add() {
            return 0;
        }
    }

    运行结果:

    com.iweb.spring5.service.impl.UserServiceImpl@3d121db3
    com.iweb.spring5.dao.impl.UserDaoImpl@3b07a0d6

    9.12.3 @Resource

    根据@Resource注解自动注入属性:

    根据类型注入

    @Service(value="userService")
    public class UserServiceImpl implements UserService {
        @Resource
        private UserDao userDao;
        @Override
        public int add() {
            System.out.println(userDao);
            return 0;
        }
    }
    
    @Repository
    public class UserDaoImpl implements UserDao {
        @Override
        public int add() {
            return 0;
        }
    }

    根据名称注入:

    @Service(value="userService")
    public class UserServiceImpl implements UserService {
        @Resource(name="userDao")
        private UserDao userDao;
        @Override
        public int add() {
            System.out.println(userDao);
            return 0;
        }
    }
    
    @Repository(value="userDao")
    public class UserDaoImpl implements UserDao {
        @Override
        public int add() {
            return 0;
        }
    }

    9.12.4 @Value

    直接注入值

    @Repository(value = "userDao")
    public class UserDaoImpl implements UserDao {
        @Value(value = "tom")
        private String name;
        @Value("20")
        private int age;
    
        @Override
        public int add() {
            System.out.println("name:" + name + ",age:" + age);
            return 0;
        }
    }

    配置的值写在外部配置文件中:

    1. 编写外部配置文件 data.properties

      name=tom
      age=20
    2. xml文件中导入配置文件

      <?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: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/context http://www.springframework.org/schema/context/spring-context.xsd">
      
          <context:property-placeholder location="classpath:data.properties" />
          <context:component-scan base-package="com.iweb.spring5" />
      </beans>
    3. 使用表达式注入值

      @Repository(value = "userDao")
      public class UserDaoImpl implements UserDao {
          @Value(value = "${name}")
          private String name;
          @Value("${age}")
          private int age;
      
          @Override
          public int add() {
              System.out.println("name:" + name + ",age:" + age);
              return 0;
          }
      }

    9.13.完全注解式开发

    在Spring中,可以实现不基于XML配置(SpringBoot推荐的方式),进行完全注解式的开发

    实现步骤:

    1. 定义配置类, 并标示配置类(添加@Configuration注解,开启组件扫描)

      @Configuration
      @ComponentScan(basePackages={"com.iweb.spring5"})
      public class SpringConfig {
          
      }
    @Component
    public class User {
        private Integer id;
        private String name;
    
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    1. 测试

      @Test
      public void testConfig(){
          ApplicationContext context =
              new AnnotationConfigApplicationContext(SpringConfig.class);
          User user = context.getBean("user", User.class);
          System.out.println(user);
      }
    com.iweb.spring5.User@72a7c7e0

    10 Mybatis

    10.1 加载依赖

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.5</version>
    </dependency>

    10.2 xml方式

    10.2.1 配置文件

    mybatis-config.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <!--通过这个配置文件,完成mybatis与数据库的连接  -->
    <configuration>
        <!--mybatis所操作的实体类 位于我们项目的哪一个包
        用于后续指定返回类型的时候 省略包名
        -->
        <typeAliases>
            <package name="com.iweb.pojo" />
        </typeAliases>
        <!--配置mybatis环境 配置我们所使用的的数据源-->
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"></transactionManager>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver" />
                    <property name="url" value="jdbc:mysql://39.106.106.39:3306/mybatis?characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC" />
                    <property name="username" value="admin" />
                    <property name="password" value="12345" />
                </dataSource>
            </environment>
        </environments>
        <!--在mybatis中 每一个实体类都有一个对应的xml文件 这个xml文件负责提供数据库操作所需执行的sql语句-->
        <mappers>
            <!--默认情况 idea在读取配置文件的时候 都是基于你所设置的resources目录作为相对路径进行读取-->
            <mapper resource="Category.xml"></mapper>
            <mapper resource="Product.xml"></mapper>
            <mapper resource="Order.xml"></mapper>
            <mapper resource="OrderItem.xml"></mapper>
        </mappers>
    </configuration>

    10.2.2 初始化

    static String mybatis_config = null;
        static InputStream inputStream = null;
        static SqlSessionFactory sqlSessionFactory = null;
        static SqlSession sqlSession = null;
    
        @Before
        public void init() throws Exception {
            System.out.println("初始化完成");
    //定义mybatis配置文件的路径名称
            mybatis_config = "mybatis-config.xml";
    //    通过流去加载该配置文件的数据
            inputStream = Resources.getResourceAsStream(mybatis_config);
    // 借助流 去创建mybatis的一级缓存对象 sqlSessionFactory(主要用来创建二级缓存对象
    // 而 二级缓存对象 才是我们执行sql语句所需要使用到的对象
    // )
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            //借助一级缓存对象 创建出二级缓存对象 该对象才是执行sql语句的主体
            sqlSession = sqlSessionFactory.openSession();
    
        }

    以Category为例

    Category实体类

    需要提供对应的getter和setter

    @Data
    public class Category {
        private int id;
        private String name;
    //    用来获取每个分类所对应的商品集合
        List<Product> products;
    }

    category.xml配置文件

    需要在mybatis-config.xml文件中mapper进行注册

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--代表的是命名空间 后续调用sql语句的时候 会用到其中定义的sql语句-->
    <mapper namespace="com.iweb.pojo">
    
    </mapper>

    10.2.3 insert

    mybatis中 传入的参数parameterType类型分类两种
    1.基本数据类型 int string long Date;
    2.负责数据类型 类 Map
    如何去获取参数的值
    基本数据类型 #(value) 或者$(value) 去获取参数值
    负责数据类型 #(属性名) 或者$(属性名) 而map 则是根据#(key)或者$(key进行获取)

        <insert id="addCategory" parameterType="Category">
    -- 从传入的Category参数中 取出其name属性 等价于 category.getName();
            insert into category ( name ) values (#{name})
        </insert>

    10.2.4 delete

    <delete id="deleteCategory" parameterType="Category" >
        delete from category where id = #{id}
    </delete>
    <!--获取单个对象 需要定义返回类型 -->
    <!--mybatis中 如果参数类型为Integer类型 这里默认写int
    但是 如果参数类型为int类型 需要使用_int 来代表参数类型
    -->

    10.2.5 update

    <update id="updateCategory" parameterType="Category">
        update category set name = #{name} where id = #{id}
    </update>

    10.2.6 select

    10.2.6.1 获取单个数据

    <select id="listCategory" resultType="Category">
        select * from category
    </select>

    10.2.6.2 获取多个数据

    <!--<!–如果你的参数 为多个数据 则参数类型需要替换为map类型-->
    <!--通过传入的map的key 获取value 而value 才是我们真正想要的参数值-->
    <!--–>-->

    <select id="getCategory" parameterType="_int" resultType="Category">
        select * from category where id = #{id}
    </select>
    <!--模糊查询-->
    <select id="listCategoryByName" parameterType="string" resultType="Category" >
      select * from category where name like concat('%',#{0},'%')
    </select>
    <select id="listCategoryByIdAndName" parameterType="map" resultType="Category">
    select * from category where id >#{id} and name like concat('%',#{name},'%')
    </select>

    10.2.7 实体间一对多关系

    <!--resultMap标签是为了定义 查询语句返回值中 具体的属性名称 和属性对应的值类型-->

    <resultMap id="categoryBean" type="Category">
            <id column="cid" property="id" />
            <result column="cname" property="name" />
            <!--声明一对多的关系
            property 指定的是集合属性的值 ofType指的是集合中元素的数据类型
            -->
            <collection property="products" ofType="Product">
                <id column="pid" property="id" />
                <result column="pname" property="name" />
                <result column="price" property="price" />
            </collection>
        </resultMap>
        <!--通过连接查询关联分类和产品-->
        <select id="listCategory" resultMap="categoryBean">
    select c.id 'cid',c.name 'cname',p.id 'pid',p.name 'pname',p.price 'price'
     from category c left join product p on c.id=p.cid
        </select>

    10.2.8 实体间多对一关系

    <!--查询所有订单 以及关联的订单项 以及订单项关联的商品商品信息-->
    <resultMap id="orderBean" type="Order" >
        <id column="oid" property="id"></id>
        <result column="code" property="code" />
        <collection property="orderItems" ofType="OrderItem">
            <id column="oiid" property="id" />
            <result column="number" property="number" />
            <association property="product"  javaType="Product">
                <id column="pid" property="id" />
                <result column="pname" property="name"  />
                <result column="price" property="price" />
            </association>
        </collection>
    </resultMap>

    10.3 注解方式

    10.3.1 实体类

    @Data
    //增加序列化接口的实现 以支持二级缓存
    public class Book implements Serializable {
        private Integer id;
        private String name;
        private Author author;
    }

    10.3.2 Mapper接口

    public interface UserMapper {
        @Select("select id,name,age from user where id = #{id}")
    //   @Results注解 相当于是xml配置中的resultMap id在注解配置中 可以省略
        // 如果id不写 value也可以不写 直接写{}提供@Reuslt即可
        //即将写在里面的@Result注解 专门负责提供映射关系
    //    由于mybatis可以自动识别数据类型 注解中的javaType和jdbcType可以省略不写 id=true 同样
        @Results(id ="userMap",value = {
                @Result(property = "uid",column = "id",javaType = Integer.class,jdbcType = JdbcType.INTEGER,id = true),
                @Result(property = "username",column = "name"),
                @Result(property = "uage",column = "age")
    
        })
        public User selectById(int id);
    
        @Select("select id,name,age from user")
    //    该语句的查询结果 是需要定义映射关系的 而该查询的映射关系 和上方的方法所定义的映射关系完全一致
        // 所以可以根据上方的@Reuslts注解所提供的id  直接进行映射关系的服用 不需要重新编写
        @ResultMap("userMap")
        public List<User> selectAll();
    }

    10.2.3 insert

    @Insert("insert into category (name) values (#{name})")
    public int add(Category category);

    10.2.4 delete

    @Delete(" delete from category where id = #{id}")
    public void delete(int id);

    10.2.5 update

    @Update("update category set name = #{name} where id = #{id}")
    public int update(Category category);

    10.2.6 select

    @Select(" select * from category ")
    public List<Category> list();

    10.2.6.1 实体间一对多关系

    @Select("select id,name from author where id=#{authorId}")
    @Results(id = "authorWithBooks",value = {
            @Result(property = "id",column = "id"),
            @Result(property = "name",column = "name"),
    //        property属性 指定关联查询的结果 封装到Author对象的books属性
            // column在执行@Many注解所定义的sql语句时候 将author表的id字段 作为参数进行传入
            // many 属性 指定@Many 通过注解定义关联查询的语句是BookMapper中的 findBooksByAuthorId
            @Result(property = "books",column = "id",many = @Many(select = "com.iweb.mapper.BookMapper.findBooksByAuthorId"))
    })
        public Author findAuthorWithBooksByAuthorId(@Param("authorId") int authorId);
    @Select("select id,name,author_id from book where author_id = #{authorId}")
    @Results(id = "booksWithoutAuthor", value = {
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name")
    })
    public List<Book> findBooksByAuthorId(@Param("authorId") int authorId);

    10.2.6.2 实体间多对一关系

        //    根据小说的id查询对应的book对象 同时通过@One注解 关联查询出作者信息Author属性
        @Select("select id,name,author_id from book where id = #{bookId}")
        @Results(id = "bookWithAuthor", value = {
                @Result(property = "id", column = "id"),
                @Result(property = "name", column = "name"),
    //            property属性 指定将关联查询的结果 封装到Book对象的author属性上
                //column制定了 在执行@One注解定义的select语句的时候 把book表的author_id作为参数进行传入
                // one属性 通过@One注解定义关联查询的语句是AuthorMapper中的findAuthorByAuthorId方法
                @Result(property = "author", column = "author_id", one = @One(
                        select = "com.iweb.mapper.AuthorMapper.findAuthorByAuthorId"
                ))
        })
    //    如果使用@Param注解 来声明参数的时候 使用#{} 或者是${} 都可以进行参数引用
        // 如果不写该注解 只能使用#{}
        // 如果不使用@Param注解 方法的参数只能有一个 并且必须是实体类(如果需要使用多个参数 就必须使用Param注解)
        //@Param注解 会让mybatis自动将参数的类型封装成map类型  @Param("bookId") 就作为map的key
        // 方便后续参数数据的获取
        @Select("select id,name from author where id=#{authorId}")
    //    根据作家id查询对应的Author信息
        public Author findAuthorByAuthorId(int authorId);

    10.3 日志打印

    10.3.1 加载依赖

    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>

    10.3.2 配置文件.properties

    # 配合mybatis 输出sql语句的执行情况和查询结果
    # log4j 全局配置
    log4j.rootLogger=ERROR, stdout
    # mybatis打印相关配置
    log4j.logger.com.iweb=TRACE
    # 控制台配置
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] -%m%n

    10.4 分页

    10.5 缓存

    二级缓存 生成一级缓存 一级缓存生成对应的Mapper对象(Mapper实现类由mybatis通过动态代理隐式提供)
    由同一个一级缓存生成的Mapper对象是共享一级缓存的数据的
    在mapper执行查询的时候 如果发现要查询的数据 在缓存中 已经 存储 则会直接从缓存中进行提取
    不会再次访问数据库

    11 SpringMVC

    11.1 加载依赖

    <!-- springframework 的相关依赖 start -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
    <dependency>
      <groupId>    org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>4.3.14.RELEASE</version>
    </dependency>
    <!-- springframework 的相关依赖 end -->
    
    <!-- jsp start -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
    <!-- jsp end -->

    11.2 实体类

    @Data
    public class Product {
        private Integer id;
        private String name;
        private Float price;
    }

    11.3 Controller

    不再需要实现接口 只需要添加注解即可
    也不需要再在xml文件中 进行bean实例的注册了

    @Controller
    //@RequestMapping注解可以用在类上和方法上
    // 如果在类上编写 以为这当前控制器中的所有方法 都以该注解内的参数作为url的开头
    // springMVC中 是可以一个控制器提供多个方法 只要每个方法对应的url不同
    @RequestMapping("/index")
    public class Index1Controller {
        //    该注解的配置等价于之前simpleUrlHandlerMapping 给控制器匹配对应的url
        // 在类上已经有了@RequestMapping配置的前提上 相当于这里做的是二段url
        @RequestMapping("/user")
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            ModelAndView mav = new ModelAndView("hello");
            mav.addObject("test", "注解方式的测试数据");
            return mav;
        }
    
    
        @RequestMapping("/set")
    //    在控制器中定义的参数 spring会自动完成参数所对应的对象的注入
    //    所有在方法中参数定义的对象 默认都会被加入到mav对象中(session会被加入到mav中)
        public ModelAndView getSession(HttpSession session) {
    //        向Session中存入数据
            session.setAttribute("test", "这是我们向session中存入的测试值");
            ModelAndView mav = new ModelAndView("getSession");
            return mav;
        }
    }
    @Controller
    public class ProductController {
            @RequestMapping("/addProduct")
    //        在springMVC控制器的方法中 参数中如果类型是实体类类型 在接受到前端的数据之后
            // Product对象的实例化 spring ioc
            // 参数值的获取 和属性的注入 spring di
            // 都会被框架部分代劳
            public ModelAndView add(Product product)throws Exception{
    //        springMVC省略了参数注入 和对象创建的过程
                //        mav.addObject("product",product) 默认也被省略了
                //
                // 在进行页面跳转的时候 springMVC默认使用的是转发方式
                //如果你希望使用重定向的方式
    
                ModelAndView mav = new ModelAndView("showProduct");
                return mav;
        }
    //不使用springMVC特性 直接使用传统servlet方式获取参数
    //    public ModelAndView add(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //        Product product = new Product();
    ////        将前端的form表单中提交的数据 以键值对的方式 封装在map中
    //        Map<String, String[]> params = request.getParameterMap();
    ////        数据从map中提取出来 传递给实例化的product对象
    //        String name = (params.get("name")[0]);
    //        float price = Float.parseFloat((params).get("price")[0]);
    //        product.setName(name);
    //        product.setPrice(price);
    //
    //        ModelAndView mav = new ModelAndView("showProduct");
    //        mav.addObject("product",product);
    //        return  mav;
    //
    //    }
    
    
    }

    12 SpringBoot

    -------------完-------------