零基础30天学会Java-韩顺平

发布时间 2023-12-12 16:01:49作者: 西芹-小汤圆

第一章 概述

了解了该视频课程的大纲和Java的基本知识,Java1995年推出,目前稳定维护的有Java8和Java11版本。

JVM(Java虚拟机):JVM包含于JDK中,Java虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行"

JRE(Java运行环境):JRE=JVM+Java的核心类库。

JDK(Java开发工具包):JDK=JRE+Java的开发工具

在桌面图标里勾选此电脑就可以在桌面显示此电脑了

dos命令行快捷键:win+r

在对应文件夹上地址上输入cmd可以直接在该目录下打开命令行

在控制台,输入tab键,可以实现命令补全

本机Java8的JDK安装位置:C:\Program Files\Java\jdk1.8.0_211\bin

环境变量path的设置,增加JAVA_HOME环境变量,指向JDK的安装目录,编辑path环境变量,增加%JAVA_HOME%\bin

如果程序中含有中文的时候,使用命令行编译的时候,需要将sublime中的文件->Set File Encoding to->GBK,再保存,才能够正确编译成class文件,而GBK是根据cmd设置的编码方式相关

运行的时候不是输入Hello.java,而是Hello,因为它实际运行的是Hello这个主类。

一个源文件中最多只能有一个public类,其他类的个数不限。每一个类都就会生成一个对应的class文件。也可以将main方法写在非public类中,然后指定运行非public类,这样入口方法就是非public的main方法。

在学习新技术时我们要思考它有什么好处,能应用到什么方面。学习新技术时先关注知识点的基本语法和基本语句,暂时不需要考虑细节,然后完成一个基础项目,最后开始考虑研究技术的注意事项、使用细节、使用规范、如何优化。

java对单引号和双引号敏感,输出时是使用双引号的。

找不到文件,可能是文件名写错了,也可能没有在当前目录下。
最不好修改的是编译没有问题,但是业务逻辑错误,还有环境错误。

变量有三个基本要素:类型+名称+值

程序中的加号,当左右两边一方为字符串,则做拼接运算,运算顺序从左到右。

java转义字符

换行与回车的区别,回车后会直接到当前行的首位,而不是换下一行,而且会用当前字符覆盖掉之前的字符

符号 作用
\t 一个制表位,实现对齐功能
\n 换行符
" 一个"
' 一个'
\r 一个回车

注释

多行注释内不允许内嵌多行注释,因为第一开始与第二个的结束符号匹配,导致第一的结束符号报错

//单行注释

/*多行注释
  多行注释*/
  
//文档注释
java文件中
/**
* @author 西芹
* @version 1.0
*/
cmd中:表示将结果存放在存放D盘的temp文件下,关键字根据上面的注释写,最后写文件名
E:\java韩顺平\练习代码>javadoc -d d:\\temp -author -version Comment02.java

代码规范

  • 类、方法的注释,要以javadoc的方式来写。
  • 非Javadoc的注释,往往是给代码的维护者看的,着重告诉读者为什么这样写,如何修改,注意什么问题。
  • 源代码使用utf-8编码。
  • 行宽度不要超过80字符。
  • 代码编写有次行风格和行尾风格,推荐行尾风格。

dos命令

命令 作用
dir 查看当前目录有什么
cd 切换到其他文件夹,切换到其他盘,需要加上/D。例如E:\java韩顺平\练习代码>cd /D c:
md 新建文件夹
rd 删除文件夹
.. 到上级目录
help 查看命令的详细信息
cd 直接切换到根目录
tree 查看指定目录下的所有子级目录
cls 清屏
exit 退出DOS

第二章 变量

java的整型常量(具体值)默认为int型,声明long型常量需后加‘l'或’L‘。例如 long num = 50L

浮点数构成:浮点数=符号位+指数位+尾数位。

java的浮点型常量(具体值)默认为double型,声明为float型常量需后加'f'或'F'。例如float num = 1.1F

浮点型常量有两种表现形式:1、十进制数形式 double num = 5.12;2、科学计数法形式 double num = 2.12e2

当我们对运算结果是小数的进行相等判断时,要小心,因为可能会有精度误差,应该是以两个数的差值的绝对值在某个精度范围类判断。如果是直接查询得到的小数或者赋值是可以正常判断的。

API(应用程序编程接口)是java提供的基本编程接口(java提供的类还有相关的方法)。中文在线文档

字符类型可以直接存放一个数字,因为在java中char的本质是一个整数,例如 char c4 = 97 ,但是会输出数字对应的Unicode字符。

字符常量需要使用单引号括起来,例如 char c1 = 'a' ,双引号代表字符串。java中还运行使用转义字符''将字符转变为特殊字符串常量,例如 char c3 = '\n' 。char类型是可以进行运算的,相当于一个整数,因为它都有对应的Unicode码。

布尔类型不可以使用0或非0的整数代替false和true,这点和C语言不同。

当java程序在进行赋值或者运算时,精度小的类型自动转换为精度大的数据类型,这个就是自动类型转换。有多种类型的数据混合运算时,系统首先将所有数据转换成容量最大的数据类型,然后再进行计算。

char->int->long->float->double;byte->short->int->long->float->double。

(byte,short)和char不会自动转换,但是它们三者之间是可以进行运算的,在计算时首先转换为int类型,包括自身的运算,例如byte和byte运算结果也是int。

boolean类型不参与自动转换。

强制类型转换使用时用加上强制转换符(),例如 int i = (int)1.9 。可能造成精度丢失,需要格外注意。强制转换符号只对最近的操作数有效,往往会使用小括号提升优先级,例如 int x = (int)(10 * 3.5 + 6 * 1.9)

基本数据类型转String,使用加号和双引号即可,例如 String s1 = n1 + "" 。String转基本数据类型,使用基本数据类型的包装类调用方法parseXX方法即可,例如 int num = Integer.parseInt(s5) ,但是一定要确保能够转换成有效的数据。从String中取字符,指定需要获取的第几个元素, char c1 = s.charAt(0)

java数据类型

基本数据类型:1、数值型:整数类型:byte[1]、short[2]、int[4]、long[8];浮点类型:float[4]、double[8]。2、字符型char[2]。3、布尔型boolean[1]。

引用数据类型:类(class)、接口(inferface)、数组([])。

字符编码表

编码表 简介
ASCII 一个字节表示一个字符,一共有128个字符,上限为256个字符
Unicode 使用两个字节表示字符,汉字和字母统一占用2个字节
UTF-8 大小可变的 编码表,字母使用1个字节,汉字使用3个字节,可以使用1-6个字符表示一个符号
gbk 字母使用1个字节,汉字使用2个字节
big5码 存储繁体中文

第三章 运算符

java取余的本质,公式 a % b = a - a / b *b ,当a为小数时,公式转变为 a % b = a - (int) a / b * b

&和|是逻辑运算符,&&和||是短路运算符

x=flase 的返回结果是flase,同理 x=true 的返回结果是true。

有小数参与运算,得到结果是近似值。

算术运算符

运算符 作用
+ 正号;数字相加;字符串相加
- 负号;数字相减;
* 数字相乘
/ 数字相除
% 取模(取余)
++ 先运算后取值;先取值后运算
-- 先运算后取值;先取值再运算
//算术运算符例题
int i = 1;
i = i++;
System.out.println(i);
//输出结果为1,因为规则使用临时变量,(1)temp=i;(2)i=i+1;(3)i=temp。所以结果为1。

int i = 1;
i = ++i;
System.out.println(i);
//输出结果为2,因为规则使用临时变量,(1)i=i+1;(2)temp=i;(3)i=temp.所以结果为2。

//复合运算符进行逻辑转换
byte b=3;
b+=2;//等价于b=(byte)(b+2)
b++;//等价于b=(byte)(b+1)

标识符

标识符命名规则

  • 有26个字母,0-9,_和$组成。
  • 数字不可以开头。
  • 不可以使用关键字和保留字,但能包含关键字和保留字。
  • java中严格区分大小写,长度无限制。

标识符命名规范

  • 包名:多单词组成时所有字母都小写:aaa.bbb.ccc,例如:com.hsp.crm
  • 类名、接口名:多单词组成时,所有单词的首字母大写(大驼峰):XxxYyyZzz,例如: TankShotGame
  • 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始首字母大写(小驼峰):xxxYyyZzz,例如:tankShotGame
  • 常量名:所有字母都大写,多单词时每个单词用下划线连接:XXX_YYY_ZZZ,例如:TAX_RATE

接收键盘输入

//导入对应的包
import java.util.Scanner;
//创建Scanner对象
Scanner myscanner = new Scanner(System.in);
//调用方法,接收用户输入
String name = myscanner.next()

进制

进制 表示方式
二进制 以0b或0B开头
十进制 正常形式
八进制 以数字0开头
十六进制 以0x或0X开头,0-9及A(10)-F(15),此处的A-F不区分大小写

位运算

符号 效果
~ 取反
& 按位与
| 按位或
^ 按位异或
>> 算术右移,低位溢出,符号位不变,用符号位补溢出的高位
<< 算术左移,符号位不变,低位补0
>>> 逻辑右移,也称为无符号右移。运算规则是低位溢出,高位补0

原码、反码、补码

  • 二进制的最高位是符号位:0代表正数,1代表负数。
  • 正数的原码、反码、补码都一样。
  • 负数的反码=原码符号位不变,其他位取反。
  • 负数的补码=反码+1,负数的反码=补码+1。
  • 0的反码,补码都是0。
  • java没有无符号数,java中的数都是有符号的。
  • 在计算机运行的时候,都是以补码的方式来运算的
  • 当我们看运算结果的时候,要看他的原码。

位运算解析

//1、先得到-2的原码,由于默认为int型,4个字节,32位,原码为10000000 00000000 00000000 00000010
//2、-2的反码:11111111 11111111 11111111 11111101
//3、-2的补码:11111111 11111111 11111111 11111110
//4、取反操作:00000000 00000000 00000000 00000001,运算后的补码
//5、运算后的原码就是00000000 00000000 00000000 00000001=>1
System.out.println(~-2);//结果为1

//1、得到2的补码:00000000 00000000 00000000 00000010
//~2操作:11111111 11111111 11111111 11111101,这是运算后的补码
//3、运算后的反码:11111111 11111111 11111111 11111100
//4、运算后的原码:10000000 00000000 00000000 00000011=>-3
System.out.println(~2);//结果为-3

第四章 控制结构

三大流程控制语句:顺序控制、分支控制、循环控制。

在java中,只要有值返回,就是一个表达式。

break语句出现在多层嵌套的语句块中,可以通过标签指明要终止的是哪一层语句块。

字符串比较是否相同:"林黛玉".equals(name) 。尽量把具体对象写在前面以避免空指针。

continue语句用于结束本次循环,继续执行下一次循环。在多层嵌套的循环语句体中,可以通过标签指定要跳过哪一层循环,这个和前面的break语句类似。

当return用在方法时,表示跳出方法,如果用在main,表示退出程序。

Switch注意事项

  • 表达式数据类型,应和case后的常量类型一致,或者是可以自动转成可以相互比较的类型。
  • Switch表达式中的返回值必须是:byte,short,int,char,enum,String。
  • case子句中的值必须是常量或常量表达式,而不能是变量。
  • default子句是可选的,当没有匹配的case时,执行default。
  • Switch穿透:case中没有break语句,运行结束后直接执行下一个case的语句,不会进行判断。

for使用细节

  • for(;循环判断条件;)中的初始化和变量迭代可以写到其他地方,但是两边的分号不能省略。
  • 循环初始值可以有多条初始化语句,但要求类型一样,并且中间用逗号隔开,循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开。

编程思想

  • 化繁为简:将复杂的需求,拆解成简单的需求,逐步完成。
  • 先死后活:先考虑固定的值,然后转成灵活变化的值。

标签

  1. label1是标签,名字由程序员指定。
  2. break后指定到哪个标签就退出到哪里。
  3. 在实际的开发中,尽量不使用标签。因为会导致可读性变差。
//标签的例子
label1:
for(int j = 0;j < 4;j++){//外层for循环
label2:  
  for(int i = 0;i < 10;i++){
    if(i == 2){
      break label1;
    }
    System.out.println("i = "+i);
  }
}

第五章 数组

数组可以存放多个同一类型的数据。数组也是一种数据类型,是引用类型。

二维数组指向多个一维数组,一维数组再指向具体的值。

二维数组允许每列的元素个数不相同。

数组操作

//新建数组
int a[] = new int[3];
int[] a = new int[3];
int[] hens = {3,5,7,8.8};
//先声明,再分配
int a[];//也可以使用int[] a;
a = new int[10];//此时才真正赋予存储空间
//获取数组长度
len = hens.length

//二维数组的新建与一维的类似
int[][] arr = {{0,0,0,0,0,0},{0,0,1,0,0,0}};
int a[][] = new int[2][3];
int[] y[];
//动态初始化,列数不等
//创建二维数组,有3个一维数组,但是每个一维数组没有开空间
int[][] arr = new int[3][];
for(int i = 0;i < arr.length;i++){
  arr[i] = new int[i+1];//给每个一维数组开空间
}
//获取行数和列数
row = arr.length;
col = arr[0].length;

//练习题
String strs = new String[]{"a","b","c"}//正确
String strs = new String[3]{"a","b","c"}//错误,不可以指定数字

数组细节

  1. 数组创建后,如果没有赋值,有默认值。int、short、byte、long的默认值为0;float和double的默认值为0.0;char的默认值为\u0000;Boolean为false;String为null。
  2. 数组中的元素可以是任何数据类型。包括基本数据类型和引用类型,但是不能混用。
  3. 数组下标从0开始。
  4. 数组属引用类型,数组型数据是对象。

赋值方式

  1. 基本数据类型赋值,赋值方式为值拷贝。
  2. 数组在默认情况下是引用传递,赋的值是地址,赋值方式为引用传递。
  3. 数组在栈中存储一个指向堆的地址,堆中存储实际的值,数组赋值时,实际上是将栈中的地址进行传递,这也是为什么将数组成为引用类型。

二维数组内存示意图

未命名文件.jpg

第六章 面向对象(基础)

属性如果不赋值,有默认值,与数组相同。

由于对象也是引用类型,所以赋值也是引用传递。

方法不能嵌套定义。

方法重载:java中允许同一个类中,多个重名方法的存在,但要求形参列表不一致。

可变参数:java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。

全局变量(属性)可以不赋值,直接使用,因为有默认值,局部变量必须赋值后才能使用,因为没有默认值。我们说的局部变量一般是指在成员方法中定义的变量。

属性可以加修饰符,局部变量不可以加修饰符。

可以使用hashCode()函数内存地址转换成的整数。例如 this.hashCode()

使用Double包装类返回值,就可以返回null来提示错误信息。

类的操作

//声明类
class Cat{
  String name;
  int age;
  String color;
  //新建方法
  public void cal(int n){
    System.out.printlin("接收的信息:" + n);
  }
}
//实例化,创建对象
//cat1是对象名(对象引用),new Cat()创建的对象空间(数据)才是真正的对象。
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.cal(5);
//将cat1指针置空
cat1 = null;

类与对象的区别

  1. 类是抽象的,概念的,代表一类事物,即它是数据类型。
  2. 对象是具体的,实际的,代表一个具体事物,即是实例。
  3. 类是对象的模板,对象是类的一个实体,对应一个实例。

对象内存示意图

基本数据类型存储于堆中,引用类型存储于方法区中的常量池。

类与对象.jpg

java内存的结构分析

  1. 栈:一般存放基本数据类型(局部变量)。
  2. 堆:存放对象(Cat cat,数组等)。
  3. 方法区:常量池(常量,比如字符串),类加载信息。

java创建对象流程

  1. 先加载类信息(属性和方法信息,每个类只加载一次)。
  2. 在堆中分配空间,进行默认初始化。
  3. 把堆中地址赋给栈中的对象名,使得对象名指向对象。
  4. 进行指定初始化。

方法调用

  1. 当程序执行到方法时,就会开辟一个独立的空间(栈空间)。
  2. 当方法执行完毕,或者执行到return语句时,就会返回到调用方法的地方。
  3. 返回后,继续执行方法后面的代码。
  4. 当main方法(栈)执行完毕,整个程序退出。

返回数据

  1. 一个方法最多一个返回值,使用数组可以传递多个结果。
    public int[] returnarr(){
      int[] resArr = {1,2,3};
      return resArr;
    }
    
  2. 返回类型可以为任意类型,包含基本数据类型或引用类型。
  3. 如果方法要求有返回类型,则方法体中最后的执行语句必须为return值,而且要求返回值类型必须和return的值类型一致或兼容。
  4. 如果方法返回类型为void,则方法体中可以没有return或只写return。

成员方法传参机制

  1. 基本数据类型,传递的是值(值拷贝),形参的任何改变不影响实参。
  2. 引用类型传递的是地址(地址也是值),可以通过形参影响实参。对象也是引用类型。

递归使用规则

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
  2. 方法的局部变量是独立的,不会相互影响。
  3. 如果方法中使用的是引用类型变量(比如数组,对象),就会共享该引用类型的数据。
  4. 当方法执行完毕,或者遇到return,就返回,遵守谁调用,就把结果返回给谁,同时方法执行完毕或者返回时,该方法执行完毕。

方法重载

  1. 方法名:必须相同
  2. 形参列表:必须不同,形参类型或个数或顺序,至少有一个不同,参数名无要求
  3. 返回类型:无要求

可变参数

//可变函数声明
class HspMethod{
  //计算多个数的和
  //int...表示接受的是可变参数,类型是int,使用可变参数时,可以当作数组来使用
  public int sum(int... nums){
    int res = 0;
    for(int i = 0;i < nums.length;i++){
      res += nums[i];
    }
    return res;
  }
}

可变参数细节

  1. 可变参数的实参可为0到任意多个。
  2. 可变参数的实参可以是数组。
  3. 可变参数的本质就是数组。
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数放在最后。
  5. 一个形参列表只能出现一个可变参数。

构造器/构造方法

  1. 主要作用是完成对新对象的初始化,并不是创建对象
  2. 构造器没有返回值,也不能写void
  3. 方法名和类名要一致
  4. 参数列表和成员方法一样的规则
  5. 构造器的调用由系统完成,不能自行调用
  6. 构造器也允许重载
  7. 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器,可以使用javap指令反编译查看。例如 javap Dog.class
//构造器/构造方法声明
class Person{
  String name;
  int age;
  public Person(String name,int age){
    //使用this指定当前对象,即当前调用构造器的对象
    this.name = name;
    this.age = age;
  }
}

this使用细节

  1. this关键字可以用来访问本类的属性、方法、构造器
  2. this用于区分当前类的属性和局部变量
    public void f1(){
      String name = "smith";
      //传统方式,输出局部变量smith
      System.out.println("name=" + name);
      //this方式,输出类属性jack
      System.out.println("name=" + this.name);
    }
    
  3. 访问成员方法的语法:this.方法名(参数列表)
  4. 访问构造器语法:this(参数列表);注意只能在构造器中调用本类的另一个构造器,而且必须放置于第一条语句,因此只能用一个
  5. this不能在类定义的外部使用,只能在类定义的方法中使用。

题目解析

public class Test{
  int count = 9;
  public void count1(){
    count = 10;
    System.out.println("count1=" + count);
  }
  public void count2(){
    System.out.println("count2=" + count);
  }
  //Test类的main方法,任何一个类,都可以有自己的main方法
  public static void main(String args[]){
    //new Test()是匿名对象,只使用一次,因为没有对象名,使用后被销毁
    new Test().count1();//输出10
    Test t1 = new Test();
    t1.count2();//输出9
    t1.count2();//输出10
  }
}

第七章 面向对象(中级)

idea编译后的文件会存放在out文件夹下,src文件夹存放源码文件。

文件->设置->编辑器->代码模板:可以查看模板或者编辑模板。遇到一个问题是我没有java的模板。可以使用代码模板加快速度,例如使用 sout 加缩进自动补全为打印。

package关键字,声明当前类所在的包,需要放在类的最上面。

包的本质实际上就是创建不同的文件夹来保存类文件。

当类重名的时候,默认使用引入的类,或者通过指定包名来指出使用的类。

包的命名只能包含数字、字母、下划线、小圆点,但不能用数字开头,不能是关键字或保留字。

继承声明:class 子类 extends 父类

方法重写(覆盖):子类的方法和父类的方法的名称、返回类型和参数都一样,那么子类的该方法覆盖父类的对应方法。

多态是指方法和对象具有多种形态,可以提高代码的复用性,利于代码维护。

instanceof比较操作符,用于判断对象的运行类型是否为某类型或某类型的子类型。例子:cat instanceof Animal

多态的方法首先看运行类型,属性首先看编译类型。

多态数组:数组的定义类型为父类类型,里面保存的实际元素为子类类型。

toString方法默认返回类的全类名(包名+类名)+@+十六进制哈希值(由hashCode获取)。当直接输出一个对象时,toString方法会被默认调用。

当某个对象没有任何引用时,则JVM就认为这个对象是一个垃圾对象,会使用垃圾回收机制销毁该对象,在销毁该对象前,会先调用finalize方法。垃圾回收并不是即时的,有自己的一套算法,使用System.gc()主动调用。

在断点调试过程中是运行状态,是以对象的运行类型来执行的。

设置步进至源码:文件->构建,执行,部署->调试器->步进->取消勾选 java.*javax*

建议一段代码完成一个小功能,尽量不要混在一起。

数据校验时,可以采用找出不正确的金额条件,然后给出提示,就直接break。

idea快捷键

快捷键 作用
ctrl+d 删除当前行
ctrl+alt+向下箭头 复制当前行到下一行
alt+/ 代码补全
ctrl+/ 第一次添加注释,第二次取消注释
alt+enter 自动导入光标对应的class
ctrl+alt+L 自动格式化代码
alt+r 运行代码
alt+insert 新建构造器,get和set方法等
ctrl+h 查看类的继承关系
ctrl+b 快速定位到光标所在的方法
声明类时在后面加上var,点击enter 自动分配变量名
F7 断点调试跳入方法内
F8 逐行执行代码
F9 继续,执行到下一个断点
shift+F8 跳出方法

包的作用

  1. 区分相同名字的类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围

访问修饰符

java提供四种访问修饰符,用于控制方法和属性的访问权限,也可以修饰类,但只能使用public和默认修饰符。

访问修饰符 访问级别 同类 同包 子类 不同包
public 公开 可以 可以 可以 可以
protested 受保护 可以 可以 可以
没有修饰符 默认 可以 可以
private 私有 可以

封装的实现步骤

  1. 将属性进行私有化private,使用户不能直接修改属性。
  2. 提供一个公共的set方法,用于对属性进行判断并赋值。
  3. 提供一个公共的get方法,用于获取属性的值。

继承细节

  1. 子类继承父类的所有属性和方法,非私有的属性可以直接访问,私有的需要通过父类的公共方法使用。
  2. java所有类都是Object类的子类,Object类是所有类的父类。
  3. 子类必须调用父类的构造器,完成父类的初始化。父类构造器的调用不限于直接父类,将一直往上追溯到Object类。
  4. 如果希望指定去调用父类的某个构造器,则显式3的调用一下:super(参数列表)。super只能在构造器中使用,且必须放在构造器第一行。
  5. super()和this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器中,使用了this()方法,就不会默认调用super()方法。
  6. 子类最多只能继承一个父类(指直接继承),那么如何继承多个?让父类去继承。
  7. 子类往上寻找属性时,遇到一个私有属性不能访问,不会跳过再往上查找,而是直接报错。
//继承题目解析
class A {
  A(){
    System.out.println("a");
  }
  A(String name){
    System.out.println("a name");
  }
}
class B extends A {
  B(){
    //由于有this()方法,所以没有默认调用super()方法
    this("abc");//调用自身的构造方法
    System.out.println("b");
  }
  B(String name) {
    //默认调用了super();
    System.out.println("b name");
  }
}
//B b = new B()的输出结果为a,b name,b

方法重写细节

  1. 子类的方法的参数、方法名称要和父类方法的参数,方法名称完全相同。
  2. 子类方法的返回类型和父类方法的返回类型一样,或者是父类返回类型的子类,比如父类方法的返回类型是object,子类方法的返回类型是String。
  3. 子类方法不能缩小父类的方法权限。

对象的多态

  1. 一个对象的编译类型和运行类型可以不一致。
  2. 编译类型在定义对象时,就确定了,不能改变。
  3. 运行类型是可以变化的。
  4. 编译类型看定义时等号的左边,运行类型看等号的右边。
//animal编译类型是Animal,运行类型是Dog,相当于披着Animal皮的Dog
Animal animal = new Dog();
//因为animal运行类型是Dog,使用执行Dog的cry方法,而不是Animal的cry方法
animal.cry();
//animal的运行类型变成了Cat,编译类型仍然是Animal
animal = new Cat();

多态的向上转型

  1. 多态的前提是两个对象(类)存在继承关系
  2. 多态的向上转型的本质是父类的引用指向子类的对象
  3. 多态的向上转型的特点是可以调用父类中的所有成员(需遵守访问权限),不能调用子类的特有成员,因为在编译阶段,能调用哪些成员是由编译类型决定的,最终运行效果看子类的具体实现。
  4. 编译阶段只看编译类型,运行时只看具体的运行类型。

多态的向下转型

  1. 语法:子类类型 引用名 = (子类类型)父类引用
  2. 只能强转父类的引用,不能强转父类的对象
  3. 要求父类的引用必须指向当前目标类型的对象
  4. 当向下转型后,可以调用子类类型中的所有成员
//进行向下转型,此时cat的编译类型为Cat,运行类型也是Cat
//向下转型要求animal原来的引用指向的是Cat(第3个特点)
Cat cat = (Cat) animal;
cat.catchMouse();//此时可以调用Cat的特有方法

多态的属性

属性没有重写,属性的值看编译类型

class Base{//父类
  int count = 10;
}
class Sub extends Base{//子类
  int count = 20;
}
Base base = new Sub();
System.out.println(base.count);
//base的编译类型的Base,输出值为编译类型的10

动态绑定

  1. 当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定。
  2. 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。
class A{
  public i = 10;
  //由于B类没有sum(),于是往上找,父类A有,执行A的sum()
  public int sum(){
    return getI() + 10;
  }
  public int getI(){
    return i;
  }
}
class B extends A{
  public int i = 20;
  //getI()在B类有,执行B的getI(),将B类中的i返回
  public int getI(){
    return i;
  }
}
A a = new B();
System.out.println(a.sum());
//输出结果为30,

==和equals的对比

  1. ==是一个比较运算符,它既可以判断基本数据类型,也可以判断引用类型。判断基本数据类型是判断值是否相等;判断引用类型则是判断地址是否相等,即判断是否为同一个对象。
  2. equals方法是Object类中的方法,只能判断引用类型。默认判断地址是否相同,子类中往往重写该方法,用于判断内容是否相等。

hashCode方法

  1. 提高具有哈希结构的容器的效率
  2. 两个引用,如果指向的是同一个对象,则哈希值一定一样;如果指向不同对象,则哈希值通常不一样
  3. 哈希值主要是根据地址号,但不能完全将哈希值等价于地址

第八章 面向对象(高级)

类的单例(单个实例)模式:采用一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

当父类的某些方法需要声明,但是又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类。

接口就是给出一些没有实现的方法,封装在一起,到某个类要使用的时候,再根据具体情况将这些方法写出来,一种高层次的抽象。

类(静态)变量

  1. 类变量(静态变量)会被该类的所有对象实例共享,而且在类加载的时候就生成了。例子:public static int count;
  2. 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问。
  3. 类变量存放位置会由于JDK版本不同而产生差异。
  4. 类变量可以通过类名.类变量名或者对象名.类变量名来访问,但是java设计者推荐使用类名.类变量名方式访问。
  5. 类变量的生命周期是随着类的加载开始,随着类的消亡而销毁。

类(静态)方法

  1. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
  2. 类方法不允许使用和对象有关的关键字,比如this和super。
  3. 当方法中不涉及任何和对象相关的成员时,则可以将该方法设计成静态方法,提高开发效率。
  4. 当方法使用了static修饰符后,该方法就是静态方法,静态方法只能访问静态属性/变量和静态方法。普通方法既可以访问非静态成员,也可以访问静态成员。
  5. 类变量可以通过类名.类方法名或者对象名.类方法名来访问,但是java设计者推荐使用类名.类方法名方式访问。

main方法

  1. main方法是由Java虚拟机调用的,处于不同的类,所以该方法的访问权限必须是public。
  2. java虚拟机在执行main方法时不需要创建对象,所以该方法的必须是static。所以main方法本质上也是静态方法,遵守静态方法的规则。
  3. main方法接收String类型的数组参数,该数组中保存执行java命令时传递给所运行的类的参数。String数组是由命令行运行时传入的。idea中可以点击编辑配置->程序参数中填写。

代码块

  1. 代码块又称为初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中。但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是在加载类时或创建对象时隐式调用。
  2. 修饰符可不写或写static,使用static的为静态代码块,作用就是对类进行初始化,而且随着类的加载而执行,并且只会执行一次;否则为普通代码块,每创建一个对象就执行一次,如果只是使用类的静态成员,普通代码块并不会执行。静态代码块只能调用静态成员。
  3. 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作。如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性。
  4. 代码块的执行顺序优先于构造器。构造器的最前面其实隐含了super方法和调用普通代码块。

类的加载

  1. 创建对象实例时(new)
  2. 创建子类对象实例,父类也会被加载
  3. 使用类的静态成员时

创建对象的顺序

  1. 调用静态代码块和静态属性初始化,两者的优先级相同,按照顺序执行。
  2. 调用普通代码块和普通属性的初始化。
  3. 调用构造方法。

子类创建对象的顺序

  1. 父类的静态代码块和静态属性
  2. 子类的静态代码块和静态属性
  3. 父类的普通代码块和普通属性初始化
  4. 父类的构造方法
  5. 子类的普通代码块和普通属性初始化
  6. 子类的构造方法

单例模式实现步骤

  1. 构造器私有化,防止用户直接进行创建。
  2. 类的内部创建对象
  3. 向外暴露一个静态的方法:getInstance
  4. 代码实现
//饿汉式:在没有使用前就提前创建好对象,可以在getInstance方法中判断是否已经创建对象,改造为懒汉式
class GirlFriend{
  private String name;
  //类的内部创建对象,为了能够在静态方法中返回gf对象,需要将其修饰为static
  private static GirlFriend gf = new GirlFriend("小红");
  //构造器私有化
  private GirlFriend(String name){
    this.name = name;
  }
  //向外暴露一个静态的方法:getInstance
  public static GirlFriend getInstance(){
    return gf;
  }
}

final关键字应用

  1. final可以修饰类、属性、方法和局部变量。
  2. 当不希望类被继承,可以用final修饰。
  3. 当不希望子类的某个方法被子类覆盖/重写时,可以用final关键字修饰。
  4. 当不希望类的某个属性被修改,可以用final修饰。
  5. 当不希望局部变量被修改,可以用final修饰。

final细节

  1. final修饰的属性又叫常量,一般用XX_XX_XX来命名
  2. final修饰的属性在定义时必须赋初值,并且不能再进行修改,且只能在定义时、构造器和代码块中为其赋初值。
  3. 如果该属性是静态的,则只能在定义时和静态代码块中赋初值。不能在构造器中赋初值,因为静态变量在类加载时创建,而构造器要在对象创建时才加载。
  4. 如果一个类已经是final类了,就没有必要在将其方法修饰为final方法。
  5. final不能修饰构造方法(即构造器)。
  6. final和static往往搭配使用,效率更高,不会导致类的加载,底层编译器做了优化处理。
  7. 包装类(Integer,Double,Float,Boolean等都是final),String也是final类。
  8. 可以指定传入的形参为final。

抽象类细节

  1. 抽象类不能被实例化。
  2. 抽象类不一定要包含abstract方法,还可以有实现的方法。
  3. 当一个类中存在抽象方法时,需要将该类声明为abstract类。
  4. abstract只能修饰类和方法,不能修饰属性和其他的。
  5. 抽象类可以有任意成员,因为其本质还是一个类,比如:非抽象方法、构造器、静态方法等。
  6. 抽象方法不可以有主体。
  7. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为抽象类。所谓实现方法就是有方法体。
  8. 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的。

接口

  1. 在JDK7之前,接口中所有方法都没有方法体,都是抽象方法。
  2. JDK8后接口中可以有静态方法,默认方法,默认方法需要使用default关键字修饰,即接口中可以有方法的具体实现。
  3. 多态传递,A接口继承B接口,C实现A接口,可以有B b = new c();
  4. 在接口中,抽象方法可以省略abstract关键字。
interface Myinterface01 {
  default public void myMethod(){
    System.out.println("默认方法");
  }
  public static void t2(){
    System.out.println("静态方法");
  }
}
class A implements Myinterface01{
}

接口细节

  1. 接口不能被实例化。
  2. 接口中所有的方法都是public方法,接口中抽象方法可以不用abstract修饰。
  3. 一个普通类实现接口,就必须将该接口的所有方法都实现。在IJ代码标黄处,使用alt+enter可以一键实现。
  4. 抽象类实现接口,可以不用实现接口的方法。
  5. 一个类可以同时多个接口。
  6. 接口中的属性,只能是final的,而且是public static final修饰符。
  7. 接口中属性的访问形式:接口名.属性名。
  8. 接口不能继承其他的类,但是可以继承多个别的接口,使用extends关键字继承。
  9. 接口的修饰符只能是public和默认,这点和类的修饰符是一样的。

接口与继承的区别

  1. 当子类继承了父类,就自动的拥有了父类的功能,如果子类需要扩展功能,可以通过实现接口的方式拓展。可以理解实现接口是对java单继承方式的补充。
  2. 继承的价值主要在于:解决代码的复用性和可维护性。接口的价值主要在于:设计好各种规范,让其他类去实现这些方法。
  3. 接口比继承更加灵活,继承满足is-a的关系,而接口只需满足like-a的关系。
  4. 接口在一定程度上实现代码解耦。使用接口规范性+动态绑定。

接口练习

interface A{
  int x = 0;
}
class B{
  int x = 1;
}
class C extends B implements A{
  public void pX(){
    System.out.println(x);//x不明确,访问接口使用A.x,父类使用super().x
  }
  public static void main(String[] args){
    new C().pX();
  }
}

内部类

一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类,嵌套其他类的类称为外部类。

内部类的最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

内部类的划分

定义在外部类局部位置上(通常方法内):

  1. 局部内部类(有类名)
  2. 匿名内部类(无类名,重点)

定义在外部类的成员位置上:

  1. 成员内部类(不使用static修饰)
  2. 静态内部类(使用static修饰)

局部内部类

  1. 可以直接访问外部类的所有成员,包括私有的。
  2. 除了可以使用final修饰,不能添加访问修饰符。
  3. 内部类可以继续被同一外部类的内部类所继承。
  4. 作用域仅仅在定义它的方法或代码块中。
  5. 外部类在方法中,可以创建内部类对象,然后调用方法即可。
  6. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类.this.成员)去访问。
class Outer02{
  private int n1 = 100;
  private void m2(){
    System.out.println("Outer02 m2()");
  }
  public void m1(){
    //局部内部类是定义在外部类的局部位置,通常在方法
    class Inner02{//本质还是一个类
      //可以直接访问外部类的所有成员,包括私有的
      private n1 = 800;
      public void f1(){
        //如果外部类和局部内部类的成员重名时,默认遵循就近原则
        System.out.println("n1=" + n1);//输出800
        //如果想访问外部类的成员,则可以使用(外部类.this.成员)去访问
        System.out.println("外部类的n1=" + Outer02.this.n1);//输出100
        m2();
      }
    }
    //外部类在方法中,可以创建内部类对象,然后调用方法即可
    Inner02 inner02 = new Inner02();
    inner02.f1();
  }
}

匿名内部类

  1. 匿名内部类没有名字,底层有一个系统命名的名字,同时还是一个对象。
  2. tiger的编译类型是IA,运行类型就是匿名内部类。
  3. 底层分配的类名为外部类名+$数字,可以使用getClass()查看。
  4. 匿名内部类使用一次,就不能再使用,对象可以反复使用。
  5. 我认为匿名内部类就是为了简便重写只需使用一次的类。
  6. 匿名内部类可以当作实参直接传递,简介高效。
class Outer04{//外部类
  private int n1 = 10;
  public void method(){
    //基于接口的匿名内部类
    //以前想使用IA接口,是先写一个类实现该接口,并创建对象,如果只是使用一次,会很麻烦,此时使用匿名内部类
    //tiger的编译类型是IA,运行类型就是匿名内部类Outer04$1
    //底层分配的类名为外部类名+$次序,所以这个是Outer04$1,可以使用getClass()查看
    //jdk底层在创建匿名内部类后,马上就创建了实例,并且把地址返回给tiger。
    IA tiger = new IA(){
      @override
      public void cry(){
        System.out.println("我是老虎");
      }
    };
    tiger.cry();
    //由于有对象的性质,也可以直接调用,效果与上面的一样
    new IA(){
      @override
      public void cry(){
        System.out.println("我是老虎");
      }
    }.cry();
    
    //基于类的匿名内部类
    //father编译类型是Father,运行类型是匿名内部类Outer04$2
    //参数列表会传递给Father的构造器
    Father father = new Father("jack"){
      @override
      public void test(){
        System.out.println("匿名内部重写了test方法");
      }
    };
    father.test();
    
    //基于抽象类的匿名内部类
    Animal animal = new Animal(){
      @override
      void eat(){
        System.out.println("eat东西");
      }
    }
  }
}
interface IA{//接口
  public void cry();
}
class Father{//类
  public Father(String name){//构造器
  }
  public void test(){//测试方法
  }
}
abstract class Animal{
  abstract void eat();
}

成员内部类

  1. 成员内部类是定义在外部类的成员位置,并且没有static修饰。
  2. 可以添加任意访问修饰符,因为它的地位就是一个成员。
  3. 外部其他类访问成员内部类。
    Outer outer = new Outer();
    //方法1,相当于将new Inner()当作是outer的成员
    Outer.Inner inner = outer.new Inner();
    //方法2,在外部类中编写一个方法,返回对象实例
    Outer.Inner inner = outer.getInner();
    

静态内部类

  1. 静态内部类是定义在外部类的成员位置,使用static进行修饰。
  2. 可以访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员。
  3. 外部其他类访问静态内部类
    //方法1,因为静态内部类是可以通过类名直接访问的
    Outer.Inner inner = new Outer.Inner();
    //方法2,在外部类中编写一个方法,返回对象实例
    Outer.Inner inner = outer.getInner();
    
  4. 重名时访问外部类的成员,可以使用外部类名.成员。

第九章 枚举和注解

枚举

  1. 当我们使用enum关键字开发一个枚举类时,默认继承Enum类。
  2. 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略。
  3. 枚举对象必须放在枚举类的行首。

枚举函数

函数 作用
ordinal() 输出该枚举对象的次序,从0开始编号
values() 返回一个数组
valueof() 将字符串转换为枚举对象,要求字符串为已有的枚举对象
compareTo() 对比两个枚举对象的编号

枚举类

enum Season{
  //要求将定义常量对象写在最前面
  SPRING("春天","温暖"),WINTER("冬天","寒冷"),AUTUMN("秋天","凉爽"),SUMMER("夏天","炎热");
  private String name;
  private String desc;
  //下面写构造器和各种方法
}

注解

和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入到代码中的补充信息。

override表示重写指定的父类方法(从编译层面验证),如果父类没有该方法,则会报错。

元注解是修饰注解的注解。

常用注解

注解 作用
@override 限定某个方法,是重写父类方法,该注释只能用于方法
@Deprecated 用于表示某个程序元素(类,方法,字段,包,参数等)已经过时
@SuppressWarnings 抑制编译器警告

元注解

元注解 作用
Retention 指定注解的作用范围,有SOURCE、CLASS、RUNTINE
Target 指定注解可以在哪些地方使用
Document 指定该注解是否会在javadoc体现
Inherited 子类会继承父类注解

第十章 异常

java语言中,将程序执行中发生的不正常现象称为“异常”,开发中的语法错误和逻辑错误不是异常。

异常分为两大类,运行时异常和编译时异常。

编译时异常是必须处理的异常,是我们编写程序时需要避免的情况。

自定义异常类名继承Exception(编译异常)或RuntimeException(运行异常)。

异常处理

IJ中将该代码选中,输入快捷键ctrl+alt+t,选中try-catch可以设置异常处理。

如果异常发生了,则异常后的代码不再执行,直接进入catch代码块。

可以有多个catch异常,捕获不同的异常,但要求父类异常在后,子类异常在前,如发生异常,只会匹配一个catch。

可以使用try-finally,即不对异常进行任何处理。

子类重写的方法所抛出的异常类型要么额父类抛出的异常一致,要么为父类抛出异常类型的子类型。

//try-catch-finally异常处理方式
try{
  int res = num1 / num2;
}catch(ArithmeticException e){
  //捕获不同的异常
}catch(Exception e){
  //程序捕获到异常后,将其封装成一个Exception对象
  e.printStackTrace();
}finally{
  //不管是否捕获到异常,始终执行
}
//throws异常处理方式,返回给上一级函数处理,是系统默认的处理方式
public static void f3(){
  f5();//会被标红,因为f2抛出的是编译异常
}
public static void f2() throws FileNotFoundException{
  //使用throws抛出异常,让调用f2方法的调用者(方法)处理
}

常见运行异常

名称 原因
NullPointerException 空指针异常
ArithmeticException 数学运算异常
ArrayIndexOutOfBoundException 数组下标越界异常
ClassCastException 类型转换异常
NumberFormatException 数字格式不正确异常

第十一章 常用类

包装类

包装类是针对8种基本数据类型对应的引用类型。

jdk5以后实现了自动装箱和自动拆箱。

三元运算符是一个整体,取最高精度。

int n1 = 100;
//装箱
Integer integer1 = Integer.valueOf(n1);//手动装箱
Integer integer2 = n1;//自动装箱,底层使用的仍是valueOf方法
//拆箱
int i = integer1.intValue();//手动拆箱
int i = integer2;//自动拆箱,底层使用的仍是intValue方法

包装类与String转换

//包装类转String
Integer i = 100;
//方式一,i本身没变化
String str1 = i + "";
//方式二,使用包装类的toString方法
String str2 = i.toString();
String str3 = String.valueOf(i);
//String转包装类
String str4 = "12345";
Integer i2 = Integer.parseInt(str4);
Integer i3 = new Integer(str4);

例题

Object obj1 = true ? new Integer(1):new Double(2.0);
System.out.println(obj1);
//输出结果为1.0,因为三元运算符为一个整体,最高为Double
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
//输出FALSE,i和j此时是对象,对象相等判断地址是否相同
Integer m = 1;
Integer n = 1;
System.out.println(m==n);
//返回True,自动装箱底层是valueOf方法,在范围-128~127正确,这个数组是事先加载好的
Integer i = 127;
int j = 127;
System.out.println(i==j);
//返回True,只要有基本数据类型,判断的是值是否相同

String类

String实现了Serializable接口,说明String可以串行化,即可以在网络中传输;Comparable接口表示可以比较。

字符串的字符使用Unicode编码,一个字符(不区分字母还是汉字)占两个字节。

String是final类,不能被其他的类继承。

String有属性value[]用于存放字符串内容,值得注意的是,该属性为final,不可修改地址。

两种创建方法的区别

Srting s = 'hsp':先从常量池查看是否有“hsp”数据空间,如果有则直接指向;如果没有则重新创建,然后指向,S最终指向的是常量池的空间地址。

String s = new String("hsp"):先在堆中创建空间,里面维护了value属性,指向常量池的hsp空间。如果常量池没有“hsp”,重新创建,有则通过value指向。最终指向的是中的空间地址。可以使用intern方法返回常量池的地址。

String a = "hello"+"abc";只创建了一个对象,因为编译器会将其优化,先进行拼接。

String s = a + "hello"底层会调用StringBuilder,所以会指向中的空间地址。

String常用方法

方法 作用
equals 区分大小写
equalsIgnoreCase 忽略大小写判断内容是否相同
length 字符的个数,字符串的长度
indexOf 获取字符第一次出现的索引
lastIndexOf 获取字符最后一次出现的索引
substring 截取指定范围的子串
trim 去前后空格
charAt 获取某索引出的字符,注意不能使用str[index]方法
contact 拼接字符串
replace 替换字符串,原字符串不变化,返回的结果才是替换的
split 分割字符串
toCharArray 转换为字符数组
compareTo 比较大小
format 格式化字符串

例题

public class Test1{
  String str = new String("hsp");
  final char[] ch = {'j','a','v','a'};
  public void change(String str,char ch[]){
    str = "java";//String类的更新实际上是更改地址
    ch[0] = 'h';
  }
}
public static void main(String[] args){
  Text1 ex = new Test1();
  ex.change(ex.str,ex.ch);
  System.out.println(ex.str+"and"+ex.ch);
  //输出hspandhava
}

StringBuffer、StringBuilder类

StringBuffer是一个容器,代表可变的字符序列,可以对字符串内容进行删减。

String保存的是字符串常量,里面的值不能更改,每次String类的实际上就是更改地址,效率较低;StringBuffer值存放在堆中,每次更新实际上可以更新内容,不用每次更新地址。

StringBuilder一个可变的字符序列,提供与StringBuffer兼容的API,但不保证同步,不是线程安全。其作为StringBuffer的一个简易替换,用于字符串缓冲区被单个线程使用,速度快于StringBuffer。

String与StringBuffer转换

//String转StringBuffer
String str = "hello tom";
StringBuffer strB = new StringBuffer(str);
//StringBuffer转String
String str = strB.toString();
String str = new String(strB);

StringBuffer常用方法

方法 作用
append 在末尾追加字符
delete(start,end) 删除区间内字符
replace(start,end,string) 将start到end中的字符替换成string
indexOf 查找子串第一次出现的索引
insert 插入
length 获取长度

例题

String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);//底层调用的是AbstractStringBuilder的appendNull,返回一个null字符串
System.out.println(sb.length());//输出4
System.out.println(sb);//输出null
StringBuffer sb1 = new StringBuffer(str);//底层super(str.length()+16),length方法抛出空指针异常

Math类

方法 作用
abs 绝对值
pow 求幂
ceil 向下取整
floor 向上取整
round 四舍五入
sqrt 开平方
random 返回[0-1)的随机数
max 最大值
min 最小值

Arrays类

常用方法

方法 作用
toString 返回数组的字符串形式
sort 排序,分为自然排序和定制排序,因为数组是引用类型,所以会影响到实参
binarySearch 二分查找,要求已排序数组
copyOf 拷贝n个元素到新数组中
fill 数组填充,将原来的元素全部替换成新元素
equals 比较两个数组元素内容是否完全一致
asList 将一组数据转换为list

定制排序

//定制排序是通过传入一个接口Comparator实现定制排序规则
Arrays.sort(arr,new Comparator(){
  @Override
  public int compare(object o1,object o2){
    Integer i1 = (Integer) o1;
    Integer i2 = (Integer) o2;
    return i2 - i1;
  }
})

System类

方法 作用
exit 退出当前程序
arraycopy 复制数组元素,比较适合底层调用
currentTimeMillens 返回当前时间距离1970-1-1的毫秒数
gc 垃圾回收机制

BigInteger和BigDecimal

BigInteger适合保存比较大的整型,BigDecimal适合保存精度更高的浮点型。

//声明BigInteger
BigInteger big1 =  new BigInteger("57987978979879879");
BigInteger big2 =  new BigInteger("989898989898899879");
//在对BigInteger进行加减乘除的时候,需要使用对应的方法
BigInteger add = big1.add(big2);
BigInteger sub = big1.substract(big2);
BigInteger mul = big1.multiply(big2);
BigInteger div = big1.divide(big2);
//声明BigDecimal
BigDecimal dec1 = new BigDecimal("489.5646545644454465");
BigDecimal dec2 = new BigDecimal("8978978.564654564564544565");
//加减乘的方法与上面BigInteger相同,除法可能出现异常,结果可能无限循环小数
//在调用除法时,指定精度,ROUND_CEILING保留被除数原有的精度
BigInteger div = dec1.divide(dec2,BigDecimal.ROUND_CEILING);

日期类

第一代日期类Date

方法 作用
Date 精确到毫秒,代表特定的瞬间
SimpleDateFormat 格式和解析日期的类
Date d1 = new Date();//获取日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");//设定格式
String format = sdf.format(d1);
String s = "1996年01月01日 10:20:30 星期一";
Date parse = sdf.parse(s);//将格式化字符串转化为日期

第二代日期类Calendar

Calender是一个抽象类,并且构造器是private,可以通过getInstance()来获取实例。

存在的问题:可变性、偏移性、不可格式化、线程不安全、不能处理闰秒。

Calendar c = Calendar.getInstance();//创建日历对象,不能使用new
//获取日历对象的字段,使用get方法
System.out.println("月:"+(c.get(Calendar.MONTH)+1));//月份从0开始,所以需要加1

第三代日期类LocalDateTime

LocalDate只包含日期,LocalTime只包含时间,LocalDateTime日期和时间都包含。

LocalDateTime ldt = LocalDateTime.now();//创建对象,不能new
System.out.println("月=" + ldt.getMonth());//返回JULY
System.out.println("月=" + ldt.getMonthValue());//返回7
//使用DateTimeFormatter对象格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(""yyyy年MM月dd日 HH小时mm分钟ss秒"");
String format = dft.format(ldt);
//Instant时间戳,与Date相互转换
Instant now = Instant.now();
Date date = Date.from(now);
Instant instant = date.toInstant();
//提供plus和minus方法对当前日期进行加减操作
LocalDateTime ldt1 = ldt.plusDays(890);//890天后的时间
LocalDateTime ldt1 = ldt.minusMinutes(8);//8分钟前的时间

练习

编程技巧:对函数进行验证时,先写出正确的情况,然后取反即可。

第十二章 集合

集合可以动态保存任意多个对象,使用比较方便,并提供了一系列方便操作对象的方法。

集合主要分为单列集合(Collection)和双列集合(Map,一般以键值对形式存在)。

Collection接口

Collection.png

Collection常用方法

Collection接口没有被实例化,以实现子类ArrayList来演示。

方法 作用
add 增加元素
remove 删除元素,指定删除元素会返回布尔值,指定对象则返回被删除对象
contains 查找元素是否存在
size 返回元素个数
isEmpty 判断是否为空
clear 清空
addAll 添加多个元素
containsAll 查找多个元素是否都存在
removeAll 删除多个元素

迭代器的执行原理

快捷键itit快速生成while循环,而且ctrl+j可以查看所有快捷键。

Iterator iterator = col.iterator();//获取迭代器
while(iterator.hasNext()){//判断是否有下一元素
  Object next = iterator.next();//返回下一元素,类型为Object
}
iterator = col.iterator();//重置迭代器
//增强for循环,简化版的iterator
for(Object book:col){
  System.out.println("book=" + book);
}

List接口

List集合类中元素有序(即添加顺序和取出顺序一致),且可重复。

List集合中的每个元素都有其对应的顺序索引,支持索引,可以根据序号存取容器中的元素。

方法 作用
add(int index,Object ele) 在index位置插入ele元素
addAll(int index,Collection eles) 从index位置开始将eles的元素添加进来
get 获取指定位置的元素
indexOf 返回元素在集合中首次出现的位置
lastIndexOf 返回元素在集合中最后出现的位置
remove 删除指定位置的元素,并返回该元素
set(int index,Object ele) 设置指定位置的元素为ele,相当于替换
subList(int from,int to) 返回从from到to的子集合

ArrayList

ArrayList可以放入null,而且可以放多个。

ArrayList底层是由数组实现数据存储的,维护了一个Object类型的数组elementData[]。当创建ArrayList对象时,如果使用无参构造器,则elementData容量为0,第一次添加时则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。如果使用的是指定大小的构造器,则初始扩容elementData为指定大小,如需要再次扩容,则扩容elementData为1.5倍

ArrayList基本等同于Vector,除了ArrayList是线程不安全的(执行效率高),在多线程情况下,建议使用Vector。

transient关键字表示该属性不会被序列化。

Vector

Vector底层也是一个对象数组,维护了一个Object类型的数组elementData[]。当创建Vector对象时,如果使用无参构造器,则调用有参构造器,设置容量为10,需要扩容,则扩容elementData为2倍。如果使用的是指定大小的构造器,则初始扩容elementData为指定大小,如需要再次扩容,则扩容elementData为2倍。不过扩容大小可以自行指定。

与ArrayList不同的是,Vector是线程同步的,即线程安全的,带有synchronized关键字。

LinkedList

LinkedList底层实现了双向链表和双端队列,可以添加任意元素,且元素可以重复或为null,但线程不安全,没有实现同步。

Set接口

Set接口是无序的,即添加和取出的顺序不一致(但是取出的顺序是固定的),没有索引;不允许重复数据,使用最多只有一个null。

同Collection的迭代方式一样,因为Set接口是Collection接口的子接口,可以使用迭代器和增强for,但是不能使用索引的方式来获取。

HashSet

HashSet的底层其实是HashMap,而HashMap的底层是数组+链表+红黑树,在数据量小的时候为哈希中的拉链法,数据量大时会转化为红黑树。

在添加数据时,先得到hash值,然后会转化为索引值,找到索引位置后判断是否已有元素,没有则直接加入,有则逐个调用equals方法比较,如果相同则放弃添加,否则将其添加到最后。在java8中,如果一条链表的元素个数超过TREEIFY-THRESHOLD(默认是8),而且数组大小大于等于MIN-TREEIFY-CAPACITY(默认为64),就会进行红黑树化。

第一次添加时,table扩容到16,临界值为容量*加载因子0.75为12。到达临界值后,会扩容2倍到32,因此新的临界值等于32乘以0.75=24,以此类推。还有一种导致扩容的情况是单条链已经超越TREEIFY-THRESHOLD(默认是8),每超越一次,触发一次扩容。该容量是指元素的个数,不是只计算在第一个位置上的。

LinkedHashSet是HashSet的子类,底层是LinkedHashMap,维护了一个数组+双向链表。其使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。第一次添加时,直接将数组扩容到16,数组类型是HashMap$Node存放的结点类型是LinkedHashMap$Entry

//HashSet不能添加相同的元素
set.add(new Dog("Tom"));
set.add(new Dog("Tom"));//是可以的,因为是两个不同的元素
set.add(new String("hsp"));
set.add(new String("hsp");//是不可以的,因为按照String的equals方法判断,两者是相同的
//哈希计算公式
hash = null ? 0:(h = key.hashcode())^(h>>>16);
//相等判断,equals会根据key进行动态绑定
(k = e.key) == key || (key != null && key.equals(k))

TreeSet

TreeSet的底层是TreeMap,可以进行排序,可以通过传入一个比较器(匿名内部类)来实现排序效果。构造器把传入的比较器对象赋给TreeSet底层的TreeMap的属性this.comparator。

TreeSet treeset =  new TreeSet(new Comparator(){
  @Override
  public int compare(object o1,object o2){
    return ((String) o1).compareTo((String) o2);
  }
})

不允许传入相同(以Comparator为准)的元素,如果设定比较长度,则长度一样的不能加入。

Map(JDK8)

Map.png

Map用于具有映射关系的数据,key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中。key不允许重复,但允许为null,而value允许重复,这种情况相当于进行替换。

常用String类作为Map的key。key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value。

HashMap是Map接口使用频率最高的实现类,其没有实现同步,因此是线程不安全。

HashTable的键和值都不能为null,而且是线程安全的。HashTable的元素是HashTable$Entry类型的,使用addEntry方法添加元素。其初始容量为11,扩容为2倍+1。

Properties还可以从xxx.propertises文件(作为配置文件)中加载数据到Properties类对象,并进行读取和修改

Map常用方法

方法 作用
put 添加
remove 根据键删除映射关系
get 根据键获取值
size 获取元素个数
isEmpty 判断是否为空
clear 清除
containsKey 查找键是否存在
keySet 获取所有的键
entrySet 获取所有的关系
values 获取所有的值

Map遍历方式

  1. 先取出所有的key,通过key取出对应的value。
  2. 把所有values取出。
  3. 通过entrySet方法获取,集合存放的数据的元素类型是Entry,提供了getKey和getValue方法。
    Set set = map.entrySet();//转换成entry类型
    for(Object obj:set){
      Map.Entry entry = (Map.Entry) obj;//向下转型
      System.out.println(entry.getKey() + "-" +entry.getValue());
    }
    

Collections工具类

//Collections的方法都是静态方法,传入参数即可调用
Collections.sort(list);
方法 作用
reverse 反转List中元素的顺序
shuffle 对List中元素进行随机排序
sort 对List元素进行升序排序,可以自行指定排序接口
swap 交换List中两个元素的位置
max 返回集合中的最大元素,可以自行指定排序接口
min 返回集合中的最小元素,可以自行指定排序接口
frequency 返回集合中指定元素的出现次数
copy(List dest,List src) 将一个src复制到dest中,需要dest的大小大于等于src
replaceAll 使用新值替换List中的所有值

例题

TreeSet treeset = new TreeSet();
//add方法,因为构造器没有传入Comparator接口的匿名内部类,
//所以底层会将尝试将Person转为Comparable类型,而Person没有实现,因此会报错,解决方法是实行Comparable接口
treeset.add(new Person);

//修改了hashcode为id和name绑定,equals是判断id和name是否相同
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);//由于此时p1已被改变,哈希值改变,不能查找到原来位置,删除失败
set.add(new Person(1001,"AA"));
//p1的name发生改变,但在内部存储的位置不变,和新加入的不相同,所以能成功加入

第十三章 泛型

传统方法不能对加入到集合中的数据进行约束,而且遍历的时候,需要类型转换,如果集合中的数据量较大,对效率有影响。

泛型又称参数化类型,是JDK5出现的新特性,解决数据类型的安全性问题,在类声明或实例化时只要指定需要的具体类型即可。

泛型的作用是在类声明时通过一个标识表示类中某个属性的类型,或者某个方法的返回值类型,或者参数类型。

JUnit单元测试框架首先输入@Test,然后在后面按alt+enter,然后点击方法旁边的运行键即可。

ArrayList<Dog> arraylist = new ArrayList<Dog>();
ArrayList<Dog> arraylist = new ArrayList<>();//简写形式,推荐使用
for (Dog dog:arraylist){
  //此时可以不再使用Object类,直接使用Dog,不用进行类型转换
}

class Person<T>{
  T s;//此时s的数据类型指定的泛型决定'
  T[] ts = new T[8];//错误的,使用泛型的数组,不能初始化,因为数组在new不能确定T的类型,就无法在内存中开空间
  public Person(T s){//T可以是参数类型
    this.s = s;
  }
  public T f(){//E可以是返回类型
    return s;
  }
  public<M,R> void fly(M m,R r){
    //泛型方法,调用方法声明的泛型
  }
  public void fly1(T t){
    //不能称作泛型方法,只是使用了泛型,里面的泛型只能使用类声明的泛型
  }
  public static void m1(T t){
    //静态方法不能使用泛型,因为静态是和类相关的,所以无法得知泛型的类型
  }
}

interface IA extends IUsb<String,Double>{
  //在继承接口时指定泛型接口的类型
}
interface IUsb<U,R>{
}

泛型使用细节

  1. 泛型只能是引用类型,不可以是基本数据类型。
List<Integer> list = new ArrayList<Integer>();//True
List<int> list2 = new ArrayList<int>();//False
  1. 在给泛型指定具体类型后,可以传入该类型或其子类类型。
  2. 如果没有指定泛型,则默认是Object类。
  3. 使用泛型的数组,不能初始化,因为数组在new不能确定T的类型,就无法在内存中开空间。
  4. 静态方法不能使用泛型,因为静态是和类相关的,所以无法得知泛型的类型。

泛型的通配符

//泛型不具备继承性
List<Object> list = new ArrayList<String>();
//<?>:支持任意泛型类型
//<? extends A>:支持A类以及A类的子类,规定了泛型的上限
//<? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限

第十四章 坦克大战

写并发程序,一定要考虑清楚,该线程什么时候结束。

线程消亡了不代表它的对象会变为null。

主要实现了英雄坦克和敌人坦克的范围内移动和子弹的发射功能,还有子弹击中效果实现。

绘图

当组件第一次在屏幕显示的时候,程序会自动的调用paint()方法来绘制组件,还有在窗口最小化后在最大化;窗口的大小发生变化;repaint函数被调用。

画笔基础JPanel画笔类,主类继承JFrame画框类,这样才能画出图像。

public class DrawCircle extends JFrame{//JFrame相当于画框
    private MyPenal mp = null;
    public static void main(String[] args) {
        new DrawCircle();
    }

    public DrawCircle(){
        //初始化面板
        mp = new MyPenal();
        //将面板放入到画框中
        this.add(mp);
        //设置画框大小
        this.setSize(400,300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//点击关闭按钮后程序退出
        this.setVisible(true);//可以显示
    }
}

class MyPenal extends JPanel{
    @Override
    public void paint(Graphics g) {//绘图方法
        super.paint(g);//调用父类方法完成初始化
        //此处画椭圆的x,y不是圆心的坐标,而是椭圆形成的长方形的左上角坐标
        g.setColor(Color.BLUE);//设置颜色为蓝色
        g.drawOval(10,10,100,100);
        //绘制图片
        Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/哈哈.jpg"));
        g.drawImage(image,10,10,500,500,this);
        //绘制文字
        g.setColor(Color.red);
        g.setFont(new Font("宋体",Font.BOLD,50));
        g.drawString("北京你好",100,100);//此处的x,y位置为左下角
    }
}

常用方法

方法 作用
drawLine 画直线
drawRect 画矩形边框
drawOval 画椭圆边框
fillRect 填充矩形
fillOval 填充椭圆
drawImage 画图片,图片需要放在out文件夹下的idea文件夹下
drawString 画字符串
setFont 设置画笔的字体
setColor 设置画笔的颜色

事件处理

java事件处理是采用“委派事件模型”,当事件发生时,产生事件的对象,会将此“信息”传递给“事件的监听者”处理。

控制小球移动需要画笔实现KeyListener接口监听键盘的操作,以实现对小球的控制,同时需要在主函数中监听对应的画笔才能实现功能。

使用getKeyCode()方法可以获取键盘输入,每当发生变化时需要调用repaint方法进行图像重绘。

public class BallMove extends JFrame {
    MyPenal mp = null;
    
    public static void main(String[] args) {
        BallMove ballMove = new BallMove();
    }

    public BallMove() {
        mp = new MyPenal();
        this.add(mp);
        this.setSize(400, 300);
        //窗口JFrame可以监听键盘事件,即可以监听面板发生的键盘事件
        this.addKeyListener(mp);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

//面板,可以画小球
//KeyListener是一个监听器,可以监听键盘时间,可以点击显示上下文生成三个需要实现的方法
class MyPenal extends JPanel implements KeyListener {
    //为了小球可以移动,把左上角坐标设置为变量
    int x = 10;
    int y = 10;

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillOval(x, y, 20, 20);
    }

    //监听有字符输出时
    @Override
    public void keyTyped(KeyEvent e) {}

    //当某个键被按下时
    @Override
    public void keyPressed(KeyEvent e) {
        //System.out.println((char) e.getKeyCode() + "被按下");
        //根据用户按下的不同键,来处理小球的移动
        if (e.getKeyCode() == KeyEvent.VK_DOWN) {//向下的箭头
            y++;
        }else if(e.getKeyCode()==KeyEvent.VK_UP){
            y--;
        }else if(e.getKeyCode()==KeyEvent.VK_LEFT){
            x--;
        }else if(e.getKeyCode()==KeyEvent.VK_RIGHT){
            x++;
        }
        //让画笔重绘图像
        this.repaint();
    }

    //当某个键被释放
    @Override
    public void keyReleased(KeyEvent e) {}
}

第十五章 线程(基础)

在java中使用线程有两种方法,一种是继承Thread类,重写run方法;另一种是实行Runnable,重写run方法。从java的设计来看,两者本质上没有区别,但实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。

Thread类实现了Runnable接口的run方法。

在控制台中输入jconsole可以查看线程信息。

为啥调用的是start方法,而不是run方法,因为run方法就是一个普通的方法,没有真正的启动一个线程,只是像以前一样顺序执行。线程是通过底层的start0方法实现的。

当线程完成任务后会自动退出,除此之外还可以通过使用变量来控制run方法停止线程,即通知方式。

用户线程:也叫工作线程,当线程的任务执行完成或通知方式结束。

守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。最常见的守护线程是垃圾回收机制。

可以在方法上加上synchronized关键字来实现同步。同步方法如果没有使用static修饰,默认锁对象为this,如果方法使用static修饰,默认锁对象为当前类.class。

在代码块上加锁:同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象);而同步方法(静态的)的锁是当前类本身。

线程同步关键在于要求多个线程的锁的对象为同一个即可。

public class Thread01 {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.start();//启动线程,是start方法而不是run方法
        //如果是通过继承Runnable接口的方法,没有start方法,则要使用下面的方式
        Thread thread1 = new Thread(cat);//设计模式中的代理模式
        Thread thread2 = new Thread(cat);//多个线程共享一个资源
        thread1.start();
        thread2.start();
        cat.setDaemon(true);//设置为守护线程
    }
}

class Cat extends Thread {
    int time = 0;
    @Override
    public void run() {//重写run方法,写上自己的业务逻辑
      synchronized(this){//给该代码块加锁
        while (time < 20) {
            System.out.println("我是西芹啊" + (++time));
            Thread.sleep(1000);//休眠1秒
        }
      }
    }
}

常用方法

方法 作用
setName 设置线程名称,使其与参数name相同
getName 返回线程的名称
start 使该线程开始执行,java虚拟机底层调用该线程的start0方法
run 调用线程对象run方法
setPriority 更改线程的优先级
getPriority 获取线程的优先级
sleep 指定线程休眠的毫秒数
interrupt 中断线程,一般用于中断正在休眠的线程,此时会进入catch方法中
yield 线程的礼让,让出CPU使其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
join 线程的插队,插队的线程一旦插队成功,则肯定让先执行完插入的线程所有的任务
getState 获取线程当前状态

线程的生命周期

java线程生命周期 - 搜索结果 - 知乎.png

生命周期 介绍
NEW 尚未启动的线程
RUNNABLE 在java虚拟机中执行的线程,可细分为READY和RUNNING两种状态
BLOCKED 被阻塞等待监视器锁定的线程
WAITTING 正在等待另一个线程执行特定动作的线程
TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程
TERMINATED 已退出的线程

释放锁

释放锁情况

  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步方法、同步代码块中遇到break、return。
  3. 当前线程在同步方法、同步代码块中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步方法、同步代码块中执行了线程对象的wait()方法,当前线程释放,并释放锁。

不释放锁情况

  1. 线程执行同步方法、同步代码块时,程序调用了Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁。
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,不会释放锁。但应尽量避免使用suspend()和resume()来控制线程。

第十六章 IO文件流

当文件读取完成后应将子资源释放,避免造成浪费。

新建文件

//第一种方式:new File(String pathname)根据路径创建一个File对象
String filePath = "e:\\news1.txt";
//这里的File对象,在java程序中只是一个对象
File file = new File(filePath);

//第二种方式:new File(File parent,String child)根据父目录文件+子路径创建
File parentFile = new File("e:\\");
String fileName = "news2.txt";
File file = new File(parentFile, fileName);

//第三种方法:new File(String parent,String child)根据父目录+子路径创建
String parentPath = "e:\\";
String fileName = "news3.txt";
File file = new File(parentPath, fileName);

//只有执行了createNewFile方法才会真正的在磁盘中创建该文件
file.createNewFile();

常用方法

在java编程中,目录也被当作是一种文件。

方法 作用
getName 获取文件名称
getAbsolutePath 获取绝对路径
getParent 获取文件父级目录
length 文件大小,按字节统计
exists 文件或目录是否存在
isFile 是不是一个文件
isDirectory 是不是一个目录
mkdir 创建一级目录
mkdirs 创建多级目录
delete 删除空目录或文件
close 关闭输入输出流

输入流和输出流

抽象基类 字节流(二进制文件) 字符流(文本文件)
输入流 InputStream Reader
输出流 OutputStream Writer

InputStream常用子类:文件输入流FileInputStream、缓冲字节输入流BufferedInputStream、对象字节输入流ObjectInputStream。

可以使用String中的getBytes()方法将字符串转换为字符数组,使其可以使用write方法写入到文件中:fileOutputStream.write(str.getBytes());

FileWriter使用后,必须要关闭(close)或刷新(flush),否则写入不到指定的文件。

比较特殊的是read方法返回的是int类型,所以要使用int变量接收后进行强转后输出。

节点流和处理流

节点流可以从一个特定的数据源读取数据。处理流(又称包装流)是“连接”在已存在的流之上,为程序提供更为强大的读写功能。

节点流是底层流/低级流,直接跟数据源连接。

处理流包装节点流,使用了修饰器设计模式,不会与数据源直接相连,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出。

处理流的功能主要体现在:性能的提高,以增加缓冲的方式提高输入输出的效率;操作的便捷,处理流提供了一系列便捷大方法来一次性输入输出大批量的数据,使用更加方便。

处理流对节点流的包装使用的多态,处理流拥有自身管理的节点流的父类属性,通过动态绑定机制实现对不同节点流的统一处理。

关闭处理流时只需关闭外层流即可,因为底层会自动的去关闭节点流。

BufferedReader和BufferedWriter都是按照字符串操作的处理流,不要去操作二进制文件,可能会导致文件损坏。

序列化和反序列化

序列化是指在保存数据时,保存数据的值和数据类型;反序列化是指在恢复数据时,恢复数据的值和数据类型。

需要让某个对象支持序列化机制,则必须让其类是可序列化的,因此该类必须实现如下接口之一:Serializable和Exterenalizable。而ObjectOutputStream和ObjectInputStream提供了对基本数据类型或对象类型的序列化和反序列化的方法。

序列化后保存的文件格式,不是存文本,而是按照它的格式进行存储。

对于自定义的类而言,通过readObject方法读取到的对象的编译类型是Object类,而运行类型是本身的类型。

序列化中的类中建议添加SerialVersionUID,这个是序列化的版本号,可以提高版本的兼容性。

序列化对象时,默认将里面所有的对象进行序列化,除了static或transient修饰的成员,还有没有实现序列化接口的属性也是不会进行序列化的。

序列化具备可继承性,如果父类已经实现了序列化,则其所有子类也默认实现了序列化。

默认输入输出

System.in的编译类型是InputStream,运行类型是BufferInputStream,默认输入是键盘。

System.out的编译类型是PrintStream,运行类型是PrintStream,默认输出是显示器。

//切换输出位置
System.setOut(new PrintStream("e\\f1.txt"));
System.out.println("hello,world");

转换流

在默认情况下,我们读取文件是按照UTF-8编码。

转换流有InputStreamReader和OutputStreamWriter,可以实现将字节流转换为字符流,并为其指定编码类型(比如UTF-8、gbk、gb2312、ISO8859-1等)。

当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文问题,所以建议将字节流转换为字符流。

//首先将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream(file),"gbk");
//然后放入BufferedReader中提高读取效率,也可以将两步合成一步写
BufferedReader br = new BufferedReader(isr);

Properties类

该类是专门用于读写配置文件的集合类,键值对不需要有空格,值不需要引号,默认类型是String。

Properties properties = new Properties();
properties.load(new FileReader("src\\mysql.properties"));
properties.list(System.out);
String user = properties.getProperty("user");
System.out.println(user);
properties.setProperty("charset","utf8");
properties.store(new FileOutputStream("src\\mysql2.properties"),null);

常用方法

方法 作用
load 加载配置文件的键值对到Properties对象
list 将数据显示到指定设备
getProperty(key) 根据键获取值
setProperty(key,value) 设置键值对到Properties对象
store 将Properties中的键值对存储到配置文件,在idea中保存数据到配置文件,中文会存储为Unicode码

第十七章 网络通信

IPV4使用32位表示地址,IPV6使用128位地址。

端口范围为0-65535,而0-1024已经被占用,例如ssh占用22,ftp占用21,smtp占用25,http占用90,常用的网络程序端口号Tomcat是8080,mysql是3306,Oracle是1521,SQLserver是1433。

当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通信的,这个端口是TCP/IP进行随机分配的。

语言本身就是协议。

InetAddress类

方法 作用
getLocalHost 获取本机InetAddress对象
getByName 根据指定主机名/域名获取ip对象
getHostName 获取InetAddress对象的主机名
getHostAddress 获取InetAddress对象的地址

Socket

套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准。

通信的两端都要有Socket,是两台机器间通信的端点,网络通信其实就是Socket之间的通信。

Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。

//客户端
public class SocketTCP01Client {
    public static void main(String[] args) throws IOException {
        //1.链接服务端(IP,端口),连接本机9999端口,可以指定对应的IP地址,如果连接成功则返回socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        //2.连接上后,生产socket,通过socket。getOutputStream()得到和socket对象关联的输出流对象
        OutputStream outputStream = socket.getOutputStream();
        //3.通过输出流,写入数据到数据通道
        outputStream.write("hello,server".getBytes());
        //设置结束标志,这样对方才能回复
        //在字符流中可以使用writer.newLine()写入结束标记,但对方需要使用readLine()
        socket.shutdownOutput();
        //4.关闭流对象和socket
        outputStream.close();
        socket.close();
        System.out.println("客户端结束");
    }
}

//服务端
public class SocketTCP01Server {
    public static void main(String[] args) throws IOException {
        //1.在本机的9999端口监听,等待链接,要求本机没有其他服务在监听9999端口
        //这个ServerSocket可以通过accept()返回多个Socket对象
        ServerSocket serverSocket = new ServerSocket(9999);
        //2.当没有客户端连接9999端口时,程序会阻塞,等待链接;如果有客户连接,则会返回Socket对象,程序继续
        Socket socket = serverSocket.accept();
        //3.通过socket.getInputStream()读取客户端写入到数据通道的数据,显示
        InputStream inputStream = socket.getInputStream();
        //4.IO读取
        byte[] buf = new byte[1024];
        int readLen = 0;
        while ((readLen = inputStream.read(buf)) != -1){
            System.out.println(new String(buf,0,readLen));//根据读取到的实际长度,显示内容
        }
        //5.关闭流和socket
        inputStream.close();
        socket.close();
        serverSocket.close();//关闭
    }
}

上传图片到客户端

//文件上传服务端
public class TCPFileUploadServer {
    public static void main(String[] args) throws Exception {
        //1.服务端在本机监听8888端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //2.等待连接
        Socket socket = serverSocket.accept();
        //3.读取客户端发送的数据,通过socket得到一个输入流
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);//使用老韩的工具
        //4.将数组写入到指定的路径,就得到一个文件了
        String destFilePath = "src\\qie2.png";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
        bos.write(bytes);
        bos.close();
        //5.向客户端回复“收到图片”
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        writer.write("收到图片");
        writer.flush();//刷新内容到数据通道
        socket.shutdownOutput();//写入结束标志
        //6.关闭其他资源
        writer.close();
        bis.close();
        socket.close();
        serverSocket.close();
    }
}

//文件上传的客户端
public class TCPFileUploadClient {
    public static void main(String[] args) throws Exception {
        //1.客户端连接服务端8888,得到Socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        //2.创建读取磁盘文件的输入流
        String filePath = "e:\\qie.png";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
        //调用老韩的文件实现转换
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //3.通过socket获取到输出流,将byte数据发送给服务端
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        bos.write(bytes);//将文件的内容写入数据通道
        bis.close();
        socket.shutdownOutput();//结束标记
        //4.接受服务端回复的消息
        InputStream inputStream = socket.getInputStream();
        //使用老韩工具,将接受到的数据转换为字符串
        String s = StreamUtils.streamToString(inputStream);
        System.out.println(s);
        //5.关闭流
        inputStream.close();
        bos.close();
        socket.close();
    }
}

netstat指令

指令 作用
netstat -an 可以查看当前主机网络情况,包括端口监听情况和网络连接情况。 netstat -an|more 可以分页显示
netstat -an|more 可以分页显示
netstat -anb 查看是哪个程序在使用该端口,需要管理员权限

UDP网络编程

类DatagramSocket和DatagramPacket实现了基于UDP协议网络程序。

UDP数据报数据通过数据报套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能安全送到目的地,也不能确定什么时候可以抵达。

DatagramPacket对象封装了UDP数据,在数据报中包含了发送端的IP地址和端口号已经接收端的IP地址和端口号。UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接。

//UDP接收端
public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        //1.创建一个DatagramSocket对象,准备在9999接收数据
        DatagramSocket socket  = new DatagramSocket(9999);
        //2.构建一个DatagramPacket对象,准备接收数据,数据包最大64k
        byte[] buf = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        //3.调用接收方法,准备接收数据,将数据填充到packet中
        socket.receive(packet);
        //4.将packet进行拆包,取出数据并显示
        int length = packet.getLength();//获取实际接收到的长度
        byte[] data = packet.getData();//实际上的数据
        String s = new String(data, 0, length);
        System.out.println(s);
        //5.关闭资源
        socket.close();
    }
}

//UDP发送端
public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        //1.创建DatagramSocket对象,准备在9998接收数据
        DatagramSocket socket = new DatagramSocket(9998);
        //2.将需要发送的数据封装到DatagramPacket对象,使用ipconfig查询ip地址
        byte[] data = "hello,明天吃火锅~".getBytes();
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.116.1"), 9999);
        socket.send(packet);
        //3.关闭资源
        socket.close();
    }
}

第十八章 反射

反射可以通过外部文件设置,在不修改源码的基础上来控制程序,也符合设计模式的开闭原则。

反射机制允许程序在执行期借助反射取得任何类的内部信息(比如成员变量、构造器和成员方法等等),并能操作对象的属性及方法。反射的应用广泛,在设计模式和框架底层都会用到。

加载完类之后,在堆中就产生了一个class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象可以得到类的结构。这个Class对象就像是一面镜子,透过这个镜子可以看到类的结构,所以形象的将其称为:反射。

反射的优点是可以动态的创建和使用对象(也是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。缺点是反射基本是解释执行,对执行速度有影响。

Method和Field、Constructor对象都有setAccessible()方法,该方法作用是启动和禁止访问安全检查的开关,参数值为true表示反射的对象在使用时取消访问检查,提高反射的效率。

静态加载在编译时加载相关的库,如果没有则报错,依赖性太强。动态加载在运行时加载需要的库,如果运行时不用该类,即使不存在该类,则不报错,降低了依赖性。

类加载的时机:创建对象、子类被加载、调用类中的静态方法、通过反射。其中只有反射是动态加载,其余都是静态加载。

在反射中,如果方法有返回值,统一返回Object类型,但运行类型和方法定义的返回类型一致。

未命名文件 (1).png

反射的使用

//从配置文件中读取到类cat和需要被调用的方法methodName
//1.加载类,返回class类型的对象cls
Class cls = Class.forName(classfullpath);
//2.通过cls得到加载的类Cat的对象实例
Object o = cls.newInstance();
//3.通过cls得到你加载的Cat的methodName对应的方法"hi"的方法对象
//在反射中,可以把方法视为对象(万物皆对象)
Method method1 = cls.getMethod(methodName);
//4.通过method1调用方法,即通过方法对象来实现调用方法
method1.invoke(o);//反射机制:方法.invoke(对象)
Field nameField = cls.getField("age");
System.out.println(nameField.get(o));

创建实例

//获取User类的Class对象
Class userclass = Class.forName("User");
//调用无参构造器创建实例
Object o = userClass.newInstance();
//调用有参public构造器创建实例
Constructor constructor = userClass.getConstructor(String.class);//获取构造器
Object hsp = constructor.newInstance("hsp");
//调用非public构造器创建实例
Constructor constructor = userClass.getDeclaredConstructor(int.class,String.class);//获取构造器
constructor.setAccessible(true);//暴破,强行访问非public的构造方法
Object hsp = constructor.newInstance(100,"hsp");

反射的作用

  1. 在运行时判断任意一个对象所属的类
  2. 在运行时构造任意一个类的对象
  3. 在运行时得到任意一个类所具有的成员变量和方法
  4. 在运行时调用任意一个对象的成员变量和方法
  5. 生成动态代理

反射的主要类

这些类在java.lang.reflection

作用
java.lang.Class 代表一个类,Class对象表示某个类加载后在堆中的对象
java,lang.reflect.Method 代表类的方法,Method对象表示某个类的方法
java.lang.reflect.Field 代表类的成员变量,获取公有的成员变量
java.lang.reflect.Constructor 代表类的构造方法

Class类

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,而是系统创建的
  3. 对于某个类的Class对象,在内存中只有一份,因为类只加载一次
  4. 每个类的实例都会知道自己是由哪个Class实例所生成
  5. 通过Class对象可以完整地得到一个类的完整结构
  6. Class对象是存放在堆的
  7. 类的字节码二进制数据是放在方法区的,有的地方称为类的元数据(包括方法代码、变量名、方法名、访问权限等)。

类加载

加载阶段:JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件,也可能是jar包,甚至是网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象。

连接阶段-验证:目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证包括文件格式验证、元数据验证、字符码验证和符号引用验证。

连接阶段-准备:JVM会在该阶段对静态变量分配内存并默认初始化(对应数据类型的默认初始化值)。这些变量所使用的内存将在方法区中进行分配。

class A{
  //n1是实例变量,不是静态变量,因此在准备阶段不会分配内存
  public int n1 = 10;
  //n2是静态变量,分配内存并初始化为默认值0,而不是20
  public static int n2 = 20;
  //n3是static final是常量,和静态变量不一样,直接赋值为30
  public static final int n3 = 30;
}

连接阶段-解析:虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化:到初始化阶段才真正开始执行类中定义的java程序代码,此阶段是执行<clinit>()方法的过程。该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值语句和静态代码块中的语句,并进行合并。

虚拟机会保证一个类的<clinit()>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行该方法,其他线程都需要阻塞等待,直到活动线程执行该方法完毕。

未命名文件 (1).png

第十九章 MySQL

数据库表的一行成为一条记录,在java程序中,一行记录往往使用对象表示。

在创建数据库,表的时候,为了规避关键字,可以使用反引号解决。

delete语句不能删除某一列的数据,可以使用update设为null或者‘’。

select语句可以通过指定distinct参数来选择是否显示重复的数据。

count(*)返回满足条件的记录的行数;count('列名')也会返回满足条件的记录的某列有多少个,但是会排除为null。

like模糊查找,%表示多个任意字符,_表示单个任意字符。

为了给某个SQL语句进行效率测试,我们需要海量数据时,可以使用该方法为表创建海量数据。

union all将两个查询结果合并,不会去重;union合并时会进行去重。

可以使用auto_increment实现自增长,填入数据时只需填入null,数据会自行增长,如果填入值,则以该值为准。自增长的机制是选取当前的最大值进行加一。

insert into my_tab01 select * from my_tab01 # 自我复制
# 将my_tab01表的结构(列)复制到my_tab02中
create table my_tab02 like my_tab01;
# 修改自增长的起始值为100
alter table t25 auto_increment = 100;
select empno,ename name,sal salary from emp;# 正确,可以省略as
# 错误,Annual Salary中间有空格,编译器无法明白,改为"Annual Salary",或者中间加上下划线
select ename,sal*12 Annual Salary from emp;

索引

索引是最物美价廉的东西,不用加内存,不用改程序,不用调SQL,查询速度就可能提高百倍千倍。但是会影响更新,删除和插入的效率,因为要更新搜索二叉树。

索引的类型:主键索引(primary key),唯一索引(unique),普通索引(index),全文索引(fulltext),但一般不使用全文索引,而是使用全文搜索Solr和ElasticSearch。

较频繁的作为查询条件字段应该创建索引,唯一性太差的和更新非常频繁字段不适合单独创建索引。

# 在emp表的empno列创建索引
create index empno_index on emp(empno)
alter table emp add index empno_index(empno)
# 查询表是否有索引
show index from t25
# 删除索引
drop index empno_index on t25

数据库备份

#备份数据库,将数据库保存到指定目录:mysqldump -u 用户名 -p -B 数据库1 数据库2 > 文件名.sql
mysqldump -u root -p -B hsp_db02 hsp_db03 > d:\\bak.sql
#备份数据库中指定的表:mysqldump -u 用户名 -p 数据库 表1 表2 > 文件名.sql
mysqldump -u root -p hsp num1 num2 > d:\\bak.sql
#恢复数据库,需要先进入MySQL命令行,输入mysql -u root -p
source d:\\bak.sql
#由于备份的数据库中含有所有操作,所有可以全部执行一遍来进行恢复

数据类型(列类型)

数据默认是有符号的,添加unsigned关键字变为无符号。

Decimal[M,D],M是小数位的总数,D是小数点后面的位数。M的最大值是65,默认值是10;D的最大值为30,默认值是0。

timestamp时间戳可以指定on update current_timestamp,这样每次更新时都会自动更新为当前时间。

字符和日期型数据应包含在单引号中。

MySQL列类型.png

char与varchar

  1. char最多可存储255个字符,varchar最大可存储65532个字节,注意是字节,因此能最多能存储的字符取决于编码方式。但填入的参数都是指定字符数。
  2. char是定长,会固定占用声明的空间;varchar是变长,存储实际占用的空间,但会使用一定长度的字节存储长度。
  3. char的查询速度大于varchar。

常用函数

字符串

dual是亚元表,系统表,可以作为一个测试表使用。

函数 作用
charset 获取字符串所属字符集
concat 连接字符串,将多个列拼接成一列
instr(string,substring) 返回substring在string出现的位置,没有则返回0
ucase 转换为大写
lcase 转换为小写
left(string,length) 从字符串的左边起取length个字符
right(string,length) 从字符串的右边起取length个字符
length 获取字符串长度(字节)
replace(str,search,replace) 将str列中的search替换成replace
strcmp 比较两个字符串大小
substring(str,position,[,length]) 截取字符串,从position开始取length个字符,从1开始计数
ltrim,rtrim,trim 去除前端或后端空格

数学

函数 作用
abs 返回绝对值
bin 十进制转二进制
celling 向上取整
conv 进制转换
floor 向下取整
format 保留小数位数,四舍五入
hex 转十六进制
least 求最小值
mod 求余
rand 返回随机数,范围为[0,1],可以填入seed,生产对应的随机数

时间日期

函数 作用
current_date 当前日期
current_time 当前时间
current_timestamp 当前时间戳
date 返回datetime的日期部分
date_add(date,interval d_value d_type) 在date中加上日期或时间
date_sub(date,interval d_value d_type) 在date上减去日期或时间
datediff 两个时间差,结果返回天数
timediff 两个时间差,返回多少小时多少分钟多少秒
now 当前时间
year|month date(date)
from_unixtime 可以将unix_timestamp转换为指定格式的日期
unix_timestamp 返回从1970-1-1到现在的秒数
last_day 返回填入日期该月的最后一天

date_add和date_sub中的interval后面可以是year,month,day,hour,second,minute

加密函数

函数 作用
user 查询登录到mysql的有哪些用户,已经登录的IP
database 查询当前的数据库名称
md5 为字符串算出一个MD5的32位字符串,进行加密
password 加密函数,MySQL数据库的用户密码函数使用该函数进行加密

流程控制函数

函数 作用
if(expr1,expr2,expr3) 然后expr1为true,则返回expr2,否则返回expr3
ifnull(expr1,expr2) 如果expr1不为空,则返回expr1,否则返回expr2
when a then b when c then d else e 多分支选择

查询

在默认情况下,当两张表查询时,规则是从第一张表中取出一行和第二张表的每一行进行组合,返回结果。因此一共返回的记录数为两张表的行数之积,称为笛卡尔集。

自连接是指在同一张表的连接查询,将同一张表看做两张表,此时需要为该表起别名才能进行操作。

select * from emp worker,emp boss; #分别取别名为worker和boss
# 多列子查询,查询和Allen的deptno和job完全一样的员工
select * from emp
where (deptno,job)=(select deptno,job from emp where ename='allen') and ename!='allen'

子查询是指嵌入到其他SQL语句中的select语句,也叫嵌套查询。子查询还可以当做临时表使用。

约束

约束用于确保数据库的数据满足特定的商业规则,在MySQL中,约束包括:not null,unique,primary key,foreign key和check五种。

如果没有指定not null,则unique字段可以有多个null。

外键(foreign key)用于定义主表和从表的之间的关系:主表约束要定义在从表上,主表则必须具有主键约束或者unique约束,要求外键列数据必须在主键列存在或者是为null。

表的类型必须是InnoDB,这样的表才支持外键。

primary key(id,name)#复合主键
#指定外键关系
foreign key (class_id) references my_class(id)

check用于强制行数据必须满足的条件。Oracle和SQL server均支持check,但是MySQL5.7目前还不支持check,只做语法校验,但不会生效。在MySQL中实现check的功能,一般是在程序中控制或者通过触发器完成。

事务

当执行回退事务时,通过指定保存点可以回退到指定的点。

提交事务后会确认事务的变化,结束事务,删除保存点,释放锁,数据生效。当使用commit语句结束事务后,其他会话将可以查看到事务变化后的新数据。

如果不开启事务,在默认情况下,操作是自动提交的,不能回滚。

InnoDB存储引擎支持事务,MyISAM不支持。

事务的特性:原子性、一致性、隔离性和持久性。

# 开始事务,也可以写set autocommit=off
start transaction
# 设置保存点
savepoint a
savepoint b
# 回退到a保存点,此时就无法再回退到b点,只写rollback的话直接回退到事务开始的状态
rollback to a
# 提交操作,此时不可以回退
commit

事务隔离

多个连接开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个连接在获取数据时的准确性。如果不考虑隔离性,可能会导致:脏读、不可重复读、幻读。

脏读:当一个事务读取到另一个事务尚未提交的修改时,产生脏读。

不可重复读:同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,产生不可重复读。

幻读:同一查询在同一事务中多次出现,由于其他提交事务所做的插入操作,每次返回不同的结果集,产生幻读。

我希望看到的数据是我连接到数据库时的数据,而不可重复读和幻读影响了我能看到的数据,不同事务之间正常来说应该不会互相影响。

# 查看当前MySQL的隔离级别
select @@tx_isolation;
# 查看系统当前隔离级别
select @@global.tx_isolation
# 设置控制台的隔离级别
set session transaction isolation level read uncommitted
# 设置系统的隔离级别
set global transaction isolation level read uncommitted

隔离级别

MySQL默认的隔离级别是repeated read,一般情况下,没有特殊要求就没有必要修改。若要修改,可以在my.ini文件中写入transaction-isolation=想要的隔离级别

MySQL隔离级别 脏读 不可重复读 幻读 加锁读
读未提交(Read uncommitted) 不加锁
读已提交(Read committed) × 不加锁
可重复读(Repeatable read) × × × 不加锁
可串行化(Serializable) × × × 加锁

√表示可能出现,×表示不会出现;加锁后需要其他事务均已提交才能运行。

存储引擎

MySQL的表类型由存储引擎决定,主要包括Myisam、innoDB、Memory等。

MySQL数据表主要支持六种类型,分别是:CSV、Memory、Archive、Mge_Myisam、Myisam和InnoDB,这六种又分为两类,一类为事务安全型,例如InnoDB;其余为非事务安全型,例如MyISAM和Memory。

# 查看所有的存储引擎
show engines
# 修改存储引擎
alter table 'tb01' engine = innodb;

常用引擎比较

Myisam不支持事务也不支持外键,但其访问速度快,对事务完整性没有要求。

InnoDB提供了具有提交、回滚和崩溃恢复能力的事物安全。但是比起Myisam,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。

Memory使用存储在内存中的内容来创建表。每个Memory表只实际对应一个磁盘文件,Memory类型的表访问非常快,因为它的数据是放在内存的,而且默认使用hash索引,但是一旦MySQL服务关闭,表中的数据就会丢失掉,表的结构还在。

特点 Myisam InnoDB Memory Archive
批量插入的速度 高,内存级别 非常高
事务安全 支持
全文索引 支持
锁机制 表级 行锁 表锁 行锁
存储限制 没有 64TB 没有
B树索引 支持 支持 支持
哈希索引 支持 支持
集群索引 支持
数据缓存 支持 支持
索引缓存 支持 支持 支持
数据可压缩 支持 支持
空间使用 非常低
内存使用 中等
支持外键 支持

视图

视图是一个虚拟表,其内容由查询定义,其数据来自于对应的真实表(基表)。创建视图后到数据库去看,对应视图只有一个视图结构文件,而没有数据文件。

视图的数据变化会影响到基表,基表的数据变化也会影响视图。

视图中仍可以生成视图。

# 创建视图emp_view01,只能查询emp表的empno、ename、job和deptno信息
create view emp_view as select empno,ename,job,deptno from emp;
# 查看视图
select empno,job from emp_view
# 查看创建视图的指令
show create view emp_view01
# 删除视图
drop view emp_view01

第二十章 jdbc和连接池

jdbc为访问不同的数据库提供了统一的接口,为使用者屏蔽了细节问题。java程序员使用jdbc,可以连接任何提供了jdbc驱动程序的数据库系统,从而完成对数据库的各种操作。

jdbc的API是一系列的接口,它统一和规范了应用程序和数据库的连接、执行SQL语句,并得到返回结果等各类操作,相关类和接口在java.sql和javax.sql包中。

jdbc程序编写步骤:注册驱动,加载Driver类;获取连接,得到Connection;执行增删改查,发生SQL给数据库执行;释放资源,关闭相关连接。

mysql的连接本质上是socket连接。

jdbc连接MySQL时,如果要使用批处理功能,需要在URL中加参数?rewriteBatchedStatements=true

批处理往往和PreparedStatement一起搭配使用,既可以减少编译次数,又减少运行次数,效率大大提高。

遇到异常时,可以使用throw new RuntimeException(e);将其转换为运行异常。

新建Propertises文件时选择文件类型为Text即可。

BasicDao是专门和数据库交互的,即完成对数据库的crud操作。在BasicDao的基础上,实现一张表对应一个Dao,更好的完成功能,比如Customer表-Customer.java类-CustomerDao.java。

声明使用可变参数可以使用省略号,例如Object... parameters

idea注释中的作者和版本信息都会对程序运行有影响。

进行多表查询时,增加来自另一张表的属性需要和原来表的属性名保持一致,因为底层会通过列名来调用set方法进行赋值,或者在SQL语句中通过取别名的方法解决。

PreparedStatement

使用PreparedStatement进行预处理可以解决Statement的SQL注入问题。PreparedStatement不再使用加号拼接SQL语句,而是使用占位符和set方法,有效解决了SQL注入问题。

String sql = "select name from class where name = ? and pwd = ?";//?作为占位符
PreparedStatement preparedStatement = connection.preparedStatement(sql);
preparedStatement.setString(1,admin_name);//给第一个?号处填入值
//由于前面已经填了,此处不需要再填入sql
ResultSet resultSet = preparedStatement.executeQuery();

ResultSet

ResultSet表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。ResultSet对象保存一个光标指向其当前的数据行,光标在最初位于第一行之前,next方法可以将光标移动到下一行,当对象没有更多行时会返回false,因此可以使用while循环进行遍历。

String sql = "select * from class";
//执行SQL语句,返回单个的ResultSet对象
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
    int id = resultSet.getInt(1);//获取第一列的数据
    String name = resultSet.getNString(2);
    System.out.println("id是" + id + ";班级是" + name);
}
//4.关闭连接
resultSet.close();

jdbc操作数据库

//首先将jar文件加入到项目中,并右键选择“添加到库”
//1.注册驱动
Driver driver = new Driver();
//2.得到连接,表示通过jdbc的方式连接mysql,localhost表示主机,可以使用ip地址,
//3306是监听的端口,day27db是连接的数据库,高版本的mysql包需要写上?serverTimezone=UTC
//mysql的连接本质上是socket连接
String url = "jdbc:mysql://localhost:3306/day27db?serverTimezone=UTC";
//将用户名和密码放入到Properties对象中
Properties properties = new Properties();
//user和password是规定好的,后面的值根据实际情况填写
properties.setProperty("user", "root");//用户
properties.setProperty("password", "hsp");//密码
Connection connect = driver.connect(url, properties);
//3.执行SQL语句
String sql = "insert into class values(null,'数学')";
//用于执行静态SQL语句并返回其生成的结果的对象
Statement statement = connect.createStatement();
int row = statement.executeUpdate(sql);//如果是dml语句,返回影响的行数
System.out.println(row > 0 ? "成功" : "失败");
//4.关闭连接
statement.close();
connect.close();

连接方式

MySQL驱动在5.1.6之后可以无需Class.forName("com.mysql.jdbc.Driver")。从jdk1.5以后使用了jdbc4,不需要显式调用该方法注册驱动,而是自动调用驱动jar包下META-INF\services\java.sql.Driver文本中的类名去注册。

本质上只有两种连接方式:静态加载和动态加载。

//方式1,创建Driver对象,静态加载
Driver driver = new Driver();
//方式2,使用反射加载Driver类,动态加载,更加灵活,减少依赖性
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver)aClass.newInstance();
//方式3,使用DriverManager替代Driver进行统一管理
//先获取Driver对象,然后传入URL,用户名和密码
DriverManager.registerDriver(driver);//注册Driver驱动
Connection connection = DriverManage.getConnection(url,user,password);
//方式4,如果使用了Class.forName方法则会自动完成注册驱动,可以省略registerDriver的步骤,这是最推荐使用的
//方式5则是在方式4上进行改进,使用配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.propertises"));
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String url = properties.getProperty("url");
String driver = properties.getProperty("driver");
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);

常用API

每日工作计划_副本.png

数据库连接池

传统的jdbc数据库连接使用DriverManager来获取,每次向数据库建立连接的时候都要将Connection加载到内存中,再验证IP地址,用户名和密码。需要数据库连接的时候,就向数据库要求一个,频繁的进行数据库连接操作将占用很多的系统资源,容易造成服务器崩溃。

每一次数据库连接,使用完后都得断开,如果程序出现异常而未能关闭,将导致数据库内存泄露,最终导致重启数据库。为解决传统开发中的数据库连接问题,可以采用 数据库连接池技术。

数据库连接池预先在缓冲区放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕后放回去。连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立。

常见连接池

jdbc的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口由第三方提供实现。

连接池 特点
C3P0 速度相对较慢,稳定性不错,hibernate和spring使用
DBCP 速度相对C3P0较快,但不稳定
Proxool 有监控连接池状态的功能,稳定性较C3P0差一点
BoneCP 速度快
Druid(德鲁伊) 来自阿里,集上面的优点于一身

连接池使用

//将c3p0提供的c3o0.config.xml拷贝到src目录下,该文件指定了数据库和连接池的相关参数
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("hsp_edu");//填入数据源名称
Connection connection = comboPooledDataSource.getConnection();
connection.close();

//1.加入Druid的jar包和配置文件druid.properties,将该文件拷贝到项目的src目录
//2.创建Properties对象,读取配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("src\\druid.properties"));
//3.创建一个指定参数的数据库连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
Connection connection = dataSource.getConnection();
connection.close();

DBUtils

关闭connection后,resultSet结果集无法使用,而且resultSet不利于数据的管理。

commons-dbutils是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的封装,使用dbutils能极大简化jdbc编程的工作量。

QueryRunner类:封装了SQL的执行,是线程安全的。可以实现增、删、改、查、批处理。

ResultSetHandler接口:用于处理java.sql.ResultSet,将数据按要求转换为另一种形式。

一定要给java类一个无参构造器,可能反射会需要。

int,double等在java中都使用包装类,因为mysql中所有类型都可能是null,而只有java是引用类型才有null值。

//DBUtils使用
Connection connection = JDBCUtilsByDruid.getConnection();
QueryRunner queryRunner = new QueryRunner();
String sql = "select * from course where cid >= ?";
//new BeanListHandler<>(Actor.class):将resultSet->Actor对象->封装到ArrayList,底层会使用反射机制
//参数1是填入到SQL中的问号
//底层得到的resultSet和PreparedStatement会在query方法中关闭
 List<Actor> list = queryRunner.query(connection, sql, new BeanListHandler<>(Actor.class), 1);
for (Actor actor : list) {
    System.out.println(actor);
}
JDBCUtilsByDruid.close(null,null,connection);//释放资源

常用函数

函数 作用
ArrayHandler 将结果集中的第一行数据转成对象数组
ArrayListHandler 将结果集中的每一行数据都转成一个数组,再存放到List中
BeanHandler 将结果集中的第一行数据封装到一个对应的javaBean实例中
BeanListHandler 将结果集中的每一行都封装到一个对应的javaBean实例中,存放到List中
ColumnListHandler 将结果集中某一列的数据存放到List中
KeyedHandler(name) 将结果集中每行的数据都封装到Map里,再把这些map存到另一个Map里,其key为指定的key
MapHandler 将结果集中第一行的数据都封装到Map里,key是列名,value是对应的值
MapListHandler 将结果集中每行的数据都封装到Map里,然后存放到List
ScalarHandler 返回单行单列,一个object对象

第二十一章 正则表达式

在java的正则表达式中,两个\\代表其他语言中的一个\。而需要用到转义字符的有:.*()$/\?[]^{}

正则表达式可以使用括号进行分组,group(0)表示匹配到的字符串,group(n)表示返回对应的分组。

java匹配默认贪婪匹配,即尽可能多的匹配。

分组的内容被捕获后,可以在这个括号后被使用,从而写出比较实用的匹配模式,这个被称作反向引用。这种引用可以在正则表达式内部,也可以是外部,内部反向引用使用\\分组号,外部引用使用$分组号。

字符串配合正则表达式使用replaceAll、matches和split方法可以提高效率。

//先创建一个Pattern对象,模式对象,可以理解为就是一个正则表达式对象
Pattern pattern = Pattern.compile("[a-zA-Z+]");
//创建一个匹配器对象
Matcher matcher = pattern.matcher(content);//content是需要匹配的文本
//开始循环匹配
while(matcher.find()){
  //匹配内容和文本会放在matcher.group()
  System.out.println("找到" + matcher.group(0));
}

//捕获命名分组,(?<name>pattern)
Pattern pattern = Pattern.compile("?(<name1>\\d\\d)(?<name2>\\d\\d)");
matcher.group("name1")

//应用实例
String regStr = "^[\u0391-\uffe5]+$";//匹配汉字
boolean matches = Pattern.matches(regStr,content);//调用静态方法进行整体匹配,而不是其中一部分
//匹配五个连续相同的数字,相当于"(//d)//1//1///1//1",意思是后面的元素与第一分组的内容相同
String regStr = "(\\d)\\1{4}";
//匹配四位长度的回文数字,意思是先与第2分组相同,再与第1分组相同
String regStr = "(\\d)(\\d)\\2\\1";
content = matcher.replaceAll("$1");//反向引用使用第1分组的内容进行替换

字符串匹配符

菜鸟教程字符串匹配符

字符 含义 示例 说明 匹配输入
. 匹配处\n以外的任意字符 a..b 以a开头,b结尾,中间包括两个任意字符为4的字符串 aaab,a35b
[] 可接收的字符列表,特殊符号将按照本身进行匹配 [efg?.] e,f,g,?和.中的任意一个字符 .,?,e
[^] 不接收的字符列表 [^efg] 除e,f,g的任意一个字符 a
- 连字符 A-Z 任意大写字母 A
| 选择匹配符,匹配'|'之前或之后的表达式 ab|cd ab或cd ab
* 指定字符重复0次或n次 (abc)* 仅包含任意个abc的字符串,等效于\w* abcabc
+ 指定字符重复1次或n次 m+(abc)* 以至少一个m开头,后接任意个abc的字符串 m
? 指定字符重复0次或1次,当此字符紧随其他限定符(*,+,?,{n},{n,},{n,m})之后,则表示采用非贪婪匹配 m+abc? 以至少一个m开头,后接ab或abc的字符串 mab
^ 指定起始字符 [1]+[a-z]* 以至少一个数字开头,后接任意个小写字母的字符串 123dsd,而a123dsd就不能匹配
$ 指定结束字符 [2]\-[a-z]+$ 以1个数字开头后接连字符‘-’,并以至少一个小写字母结尾的字符串 1-a
接收n个字符 [abcd] 由abcd中字母组成的任意长度为3的字符串 abc
指定至少n个匹配 [abcd] 由abcd中字母组成的任意长度不小于3的字符串 abaaaaa
指定至少n个但不多于m个匹配 [abcd] 由abcd中字母组成的任意长度不小于3,不大于5的字符串 aaaa
\\d 匹配单个数字字符,相当于[0-9] \\d{3}(\\d)? 包含3个或4个数字的字符串 123,7895
\\D 匹配单个非数字字符,相当于[^0-9] \\D(\\d)* 以单个非数字字符开头,后接任意个数字字符 a,A345
\\w 匹配单个数字、大小写字母字符,相当于[0-9a-zA-Z] \\d{3}\\w 以3个数字字符开头的长度为7的数字字母字符串 234abcd
\\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \\W+\\d 以至少一个非数字字母字符开头,2个数字字符结尾的字符串 #29
\\s 匹配任意空白字符(空格、制表符等) \\s 任意空白字符 一个空格
\\S 匹配任意非空白字符 \\S 任意非空白字符 A
\\b 匹配目标字符串的边界 han\\b 字符串的边界指的是子串之间有空格,或者是目标字符串的结束位置 hanshunhan nn han
\\B 匹配目标字符串的非边界 han\\B 与\\b的含义相反,匹配北边界的han hanshunhan nnhan

大小写区分

java正则表达式默认区分字母大小写,如何实现不区分大小写。

  • (?i)abc表示都不区分大小写
  • a(?i)abc表示bc不区分大小写
  • a((?i)b)c表示只有b不区分大小写
  • Pattern pat = Pattern.compile(regEx,Pattern.CASE_INSENSITIVE);

非捕获分组

非捕获分组的意思是只是进行了匹配,并没有获取该分组子表达式的值,即不能通过group(n)等获取该分组的值。

构造形式 说明
(?:pattern) 匹配pattern但不捕获该匹配式的子表达式,不存储供以后使用的匹配。该形式对于用"or"字符(|)组合模式部件的情况很有用,例如”韩顺平(?:学习
(?=pattern) "Windows(?=95|98
(?!pattern) 和第二个模式取反,该模式不匹配括号内的字符。

Matcher类

方法 作用
matches 整体匹配,返回一个布尔值
start 返回匹配的开始索引
end 返回匹配的结束索引,对应位置的后一位
replaceAll(oldStr) 返回一个替换后的结果

常用正则表达式

校验数字

1 数字:^[0-9]*$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
8 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

校验字符

1 汉字:^[\u4e00-\u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12 禁止输入含有~的字符:[^~\x22]+

特殊需求

1 Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL:[a-zA-z]+://[^\s]* 或 ^https://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$ 
6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7 身份证号:
        15或18位身份证:^\d{15}|\d{18}$
        15位身份证:^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$
        18位身份证:^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{4}$
8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ 
12 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$ 
15 钱的输入格式:
16 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$ 
17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$ 
18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$ 
19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$ 
20 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$ 
21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$ 
22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$ 
23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$ 
24 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[\u4e00-\u9fa5]
27 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
29 HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33 IP地址:\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)

  1. 0-9 ↩︎

  2. 0-9 ↩︎