# java 运行机制

  1. .java 文件是源代码,.class 文件是字节码,javac.exe 编译源代码为字节码
  2. 每有一个类,编译后都会生成一个.class 文件

JDK JRE JVM

JDK = JRE + Java 开发工具

JRE = JVM + 核心类库

JVM:Java virtual machine

# 注意事项:

  1. 源文件的组成部分是类(class)
  2. java 应用程序入口是 main () 方法,固定书写格式:public static void main (String [] args) {}
  3. 一个源文件最多只能有一个 public 类,其他不限
  4. 如果源文件包含一个 public 类,那文件名必须按该类名命名
  5. main 方法可以写在非 public 类,然后指定运行非 public 类,这样入口方法就是非 public 类的 main 方法

# 转义字符

\t:制表位,对齐,固定宽度

\r:一个回车,对齐到第一个字并逐个替换,比如 System.out.println (“我爱你 \r 他”),会输出他爱你

如果是 System.out.println (“我爱你 \r\n 他”) 就会输出两行

我爱你

# 文档注释

Javadoc 写法和标签总结_javadoc 标签大全 - CSDN 博客

在方法的上面输入

1
/**

然后回车,就会自动生成一个 javadoc 格式的文档,然后就可以填内容了

和其他注释最主要的区别就是 javadoc 的注释可以在我们鼠标放到方法名 / 类名 / 变量名上面的时候,直接看到其内容,而双斜杠的注释不行

所以按照一般来说,我们会在类 / 变量 / 方法的注释中使用 javadoc 的形式,而在某段代码的注释使用双斜杠形式

# 用法

javadoc -d 文件夹名 -xx -yy Demo3.java

假设在 d 盘且文件名为 tch,所以下面这个就是

javadoc -d d:\ -author -version tch.java

1
2
3
4
/**
* @author tch
* @version 1.0
*/

# 常用 javadoc 标签

标签 描述 标签类型
@author 作者标识 包、 类、接口
@deprecated 标识当前 API 已经过期,仅为了保证兼容性依然存在,以此告之开发者不应再用这个 API 包、类、接口、值域、构造函数、 方法
指明当前文档根目录的路径
@exception 标志一个类抛出的异常 构造函数、 方法
从直接父类继承的注释
链接到某个特定的成员对应的文档中 包、类、接口、值域、构造函数、 方法
插入一个到另一个主题的链接,但是该链接显示纯文本字体 包、类、接口、值域、构造函数、 方法
@param 方法的入参名及描述信息,如入参有特别要求,可在此注释 构造函数、方法
@return 对函数返回值的注释 方法
@see 引用,查看相关内容,如类、方法、变量等 包、类、接口、值域、构造函数、 方法
@serial 说明一个序列化属性
@serialData 说明通过 writeObject () 和 writeExternal () 方法写的数据
@serialField 说明一个 ObjectStreamField 组件 @
@since 描述文本,API 在什么程序的什么版本后开发支持 包、类、接口、值域、构造函数、 方法
@throws 构造函数或方法所会抛出的异常 构造函数、 方法
显示常量的值,该常量必须是 static 属性 静态值域
@version 版本号 包、 类、接口
对两种标签格式的说明:
  • @tag 格式的标签(不被 { } 包围的标签)为块标签,只能在主要描述(类注释中对该类的详细说明为主要描述)后面的标签部分(如果块标签放在主要描述的前面,则生成 API 帮助文档时会检测不到主要描述)。
  • {@tag} 格式的标签(由 { } 包围的标签)为内联标签,可以放在主要描述中的任何位置或块标签的注释中

# 数据类型转换细节

# 自动转换

  1. (byte short)和 char 之间不会相互自动转换
  2. byte short char 他们三者可以计算,在计算时首先转换为 int 类型
    • 哪怕是相同的类型,比如 byte 和 byte 计算,他也会先转成 int 再计算,结果类型也就是 int,另外两个也是的
  3. 布尔类型不参与类型自动转换

# 强制转换

1
2
3
4
5
6
7
8
9
10
double a = 123.5;
int b = (int)a;
//字符串强制转换
String a = "123";
int num1 = Integer.parseInt(a);
double num2 = Integer.parseDouble(a);
long num3 = Integer.parseLong(a);
//其他类型以此类推
//而char类型的强制转换就只会截取字符串的第一个字符
System.out.println(a.charAt(0));//输出a的第一个字符

# 位移运算

  1. 算术右移 >>:低位溢出,符号位不变,并用符号位补溢出的高位,可以理解为右移一位就除以 2
  2. 算术左移 <<:符号位不变,低位补 0,可以理解为左移一位就乘以 2
  3. '>>>' 逻辑右移也叫无符号右移,运算规则是:低位溢出,高位补 0

# 数组

  1. 不知道值的时候比如需要输入就用动态,已知数组的值比如用来加密的表就用静态

  2. 数组如果未赋值,每个类型都有其对应的默认值:数字类型全是 0 或 0.0(浮点),char 为 \u000,boolean 为 false,String 为 null

  3. 数组的赋值为地址复制,如果只想值复制的话,可以先分配空间后再用遍历赋值

    1
    2
    3
    4
    5
    int arr1[]={123};
    int arr2 = new int[arr1.length];
    for(int i=0;i<arr1.length;i++) {
    arr2[i] = arr1[i];
    };

# 动态初始化

int [] arr= new arr [10]; 或者 int arr [] = new arr [10];

也可以先声明再分配空间:

int arr[];

arr = new int[10];

# 静态初始化

int tch[] = {1,2,3,4,5};

# 扩容

用新数组扩容并转移地址

1
2
3
4
5
6
7
int arr[] = {1,2,3};
int newarr = new int[arr.length+1];
for(int i=0;i<arr.length;i++) {
newarr[i] = arr[i];
}
newarr[newarr.length-1] = 4;
arr = newarr;

# 二维数组

  • java 中二维数组的列数可以不确定,第一个一维可以有五个元素,第二个一维可以只有 3 个元素

    1
    2
    3
    4
    int arr[][] = new int[3][];
    for(int i=0;i<arr.length;i++) {
    arr[i] = new int[i+1];//在这里给每个一维数组分配长度
    }
1
2
3
4
5
6
7
8
9
10
int arr[][] = { {1,2,3}, {4,5,6} };//静态

int arr[][] = new int[2][3];//动态
//或者先声明
int arr[][];
arr = new int[2][3];
//还可以这样
int[] arr[] = new arr[2][3];
//这种方式可以同时定义一个一维和二维
int[] arr1,arr2[];

# 类与对象

# 创建规则

1
2
3
4
5
6
7
8
9
10
11
class Cat {
String name;
int age;
String color;
}
//使用时,先实例化再引用
Cat cat1 = new Cat();//cat1就是对象名
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
//如果不对类中的属性赋值的话,就是默认值,默认值规则和数组一样
  • 对象的复制是指向地址的

# 类和对象的内存分配机制

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

# 创建对象的流程简单分析

1
2
3
Person p = new Person();
p.name = "jack";
p.age = 10;
  1. 先加载 Person 类信息(属性和方法信息,只加载一次)
  2. 在堆中分配空间,进行默认初始化(默认值)
  3. 把地址赋给 p,让 p 指向对象
  4. 进行指定的初始化,比如上面代码的赋值

# 方法

# 如何创建和引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class tch{
public static void main(String args[]){
Personn p1 = new Person();//分配空间
p1.speak();//引用
}
}
class Person {
String name;
int age;
//创建
public void speak() {//无传入值
System.out.println("I'm a good man");
}
}

# 方法调用过程

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

# 一些细节

  1. 有多个返回值的时候可以用数组
  2. void 类型不可以 return 值,但可以只写 return
  3. 同类中的方法可以直接调用,不用创建对象

# 传参机制

  1. 基本数据类型的传参就是拷贝
  2. 引用数据类型的传参是指向地址

# 递归

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量时独立的,不会相互影响
  3. 如果方法中使用的是引用类型变量,就会共享该引用类型的数据

递归主要是对于规律的应用吧,只要微操作和超级操作是对的,就不要去想里面的过程是怎么样,两个操作都是正确的,其实也就互相证明了这个程序是对的,要相信它可以实现我们的要求

# 方法重载

java 中允许同一个类中有多个同名方法的存在,但形参列表要不一样。重载减轻了起名和记名的麻烦

1
2
3
//比如最基础的
System.out.println("123");
System.out.println("abc");
  1. 方法名要一致
  2. 形参列表必须不同,类型、个数、顺序等
  3. 返回类型:无要求

# 可变参数

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

# 基本语法

1
2
3
4
5
6
7
8
9
10
访问修饰符 返回类型 方法名(数据类型...形参名) {
}
//使用可变参数时,可以把参数当作数组来使用,也可以把数组传进来
public int sum(int... nums) {
int res = 0;
for(int i=0;i<nums.length;i++) {
res += nums[i];
}
return 0;
}

# 细节

  1. 可变参数的个数可以是 0 也可以是任意多个
  2. 可变参数的本质就是数组
  3. 可变参数可以和普通类型的参数一起放在形参列表,但是必须保证可变参数在最后,且一个形参列表中只能有一个可变参数
1
2
public void f1(int a,double b,char... c) {
}

# 作用域

  1. java 中主要的变量就是属性(成员变量)和局部变量
  2. 局部变量一般是指在成员方法中定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
class cat {
int age = 10;//age就是全局变量
public void cry() {
//这里的n和name就是局部变量
//n和name的作用域就在cry中
int n = 10;
String name = "jack"''
}
public void eat() {
System.out.print(age);//这里可以用age但不能用cry中的变量
}
}
  • java 中作用域的分类:

全局变量:也就是属性,作用域为整个类体

局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中

  • 全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值后才能使用,因为没有默认值

  • 局部变量和属性(全局变量)可以重名,访问时遵循就近原则

1
2
3
4
5
6
7
class cat {
String name = "jack";
public void cry() {
String name = "tom";//这行在就会输出tom,反之输出jack
System.out.print(name);
}
}
  • 可以跨类调用全局变量
  • 全局变量可以加修饰符,局部不可以

# 构造器 / 方法

  • 构造器的修饰符可以默认,也可以是 public,protected,private
  • 构造器没有返回值
  • 方法名和类名必须一样
  • 参数列表和成员方法一样的规则
  • 构造器的调用系统完成,在创建对象时,系统会自动调用该类的构造器完成对对象的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class constructor {
public static void main(String args[]) {
//new一个对象时,直接通过构造器指定名字和年龄
Person p1 = new Person("tch", 21);
}
}
class Person {
String name;
int age;
//构造器的名称和类名要一样
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
}
  • 构造器也可以重载,因为构造器本身也是方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class constructor {
public static void main(String args[]) {
Person p1 = new Person("tch", 21);
Person p1 = new Person("tchhhh");
}
}
class Person {
String name;
int age;
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
public Person(String pName) {//只有一个形参
name = pName;
}
}
  • 程序员如果没定义构造器,系统会自动生成一个默认无参构造器
  • 有参构造器在 new 新对象的时候要提供参数,无参构造器不需要,相当于给了多种 new 新对象的方式
1
2
3
4
5
class Person {
/* Person(); {
}
*/
}//这里是空的,但实际上有一个默认的构造器,可以通过反编译出来,javap
  • 一旦定义了自己的构造器,默认的构造器就被覆盖了,不能再使用默认无参构造器,除非显式地定义一下
1
2
3
4
5
6
7
8
class Person {
public Person(String pName, int pAge) {
//...
}
Person() {//显式的定义一下

}
}

# this 关键字

在这里定义构造器的时候,如果可以把形参定义为 String name, int age 看上去就会更简介,但是如果这样定义就会导致 name 的值为空,这里可以用 this 来解决

1
2
3
4
5
6
7
8
class Dog {
String name;
int age;
public Dog(String dName, int dAge) {
name = dName;
age = dAge;
}
}

java 虚拟机会给每个对象分配 this,代表当前对象,哪个对象调用 this 就指的是哪个对象的地址

1
2
3
4
5
6
7
8
class Dog {
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
  • this 不能在类定义的外部使用,只能在类定义的方法中使用
  • 访问成员方法的语法:this. 方法名 (参数列表)
1
2
3
4
5
6
7
8
9
class T {
public void f1() {
//...
}
public void f2() {
f1();
this.f1();
}
}
  • 访问构造器语法:this (参数列表);只能在构造器中使用(只能在构造器中访问另一个构造器)
1
2
3
4
5
6
7
8
9
public T {
public T() {
//如果要在构造器中用this访问另一个构造器
this("tch",21);//this语句必须要在第一行
}
public T(String name, int age) {
//...
}
}

#

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

# 作用:

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

# 命名规范

1
2
3
4
5
6
//一般的命名规范
com.公司名.项目名.业务模块名
//eg:
com.sina.crm.user//用户模块
com.sina.crm.order//订单模块
com.sina.crm.utils//工具类

# 常用的包

1
2
3
4
java.lang.*		//lang包时基本包,默认引入
java.util.* //系统提供的工具,工具类,使用Scanner
java.net.* //网络包,网络开发
java.awt.* //java界面开发,GUI

# 细节

  • package 的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句 package
  • import 指令位置放在 package 的下面,在类定义前面,可以有多句且没有顺序要求

# 访问修饰符

  1. 公开级别:用 public 修饰,对外公开
  2. 受保护级别:protected,对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包的类公开
  4. 私有级别:private,只有类本身可以访问,不对外公开

# 封装 encapsulation

封装就是把抽象出的数据 [属性] 和对数据的操作 [方法] 封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作 [方法],才能对数据进行操作

# 封装的好处

  1. 隐藏实现细节:方法 (连接数据库)<–调用 (传入参数)
  2. 可与对数据进行验证,保证安全合理

# 封装的实现步骤

  • 将属性进行私有化【不能直接修改属性】

  • 提供一个公共的 (public) set 方法,用于对属性判断并赋值

1
2
3
4
public void setXxx(类型 参数名){//Xxx表示某个属性
//加入数据验证的业务逻辑
属性 = 参数名;
}
  • 提供一个公共的 (public) get 方法,用于获取属性的值
1
2
3
public 数据类型 getXxx(){//权限判断,Xxx某个属性
return xx;
}

# Getter 和 Setter

Getter 和 Setter 方法可以直接在别的类中访问或修改私有属性,它们提供了封装性,检验和操作的功能,通过规范的命名和方法签名,可以提高代码的可读性和维护性

快捷键:Alt + insert(笔记本加 Fn)

# 作用

  • Getter 和 Setter 方法的使用可以有效地控制对对象属性的访问和修改,提供了封装性和安全性

  • 通过 Getter 方法,其他类可以获取属性的值而不需要直接访问属性

  • 通过 Setter 方法,其他类可以修改属性的值而不需要直接修改属性

  • 封装性:通过 Getter 和 Setter 方法,可以对属性的访问和修改进行控制

  • 验证和操作:可以在 Getter 和 Setter 方法中实现对属性的验证和操作,例如范围检查、格式转换等,确保数据的合法性

  • 可读性和可维护性:通过 Getter 和 Setter 方法可以提供更具描述性和可读性的代码,使代码更易于理解和维护

# 继承

在一些问题中可能会出现,很多类的属性和方法都一样,这就造成了代码复用的现象,代码冗余度很高,继承就可以解决代码复用性的问题。

当多个类存在相同属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可。

image-20240815120355095

# 语法

1
2
3
4
5
6
7
8
9
class 子类 extends 父类 {
}
//子类会自动拥有父类定义的属性和方法
//父类也叫超类,基类
//子类又叫派生类
public class Pupil extends Student{
public void testing() {
System.out.println(name + "正在考小学数学");
}

# 细节

  • 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过公共的方法去访问
  • 子类必须调用父类的构造器,完成对父类的初始化
  • 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则编译不会通过
1
2
3
4
5
6
7
//比如现在的父类就没有无参构造器,但是有形参为String,int的构造器,那么可以这样使用super();
public class Sub extends Base {
public Sub() {
super("jack", 10);
System.out.println("子类Sub()构造器被调用。");
}
}
  • 如果希望指定去调用父类的某个构造器,则显示的调用一下
  • super 在使用时,需要放在构造器第一行
  • super () 和 this () 都只能放在构造器的第一行,因此这两个方法不能共存在一个构造器
  • java 所有类都是 Object 类的子类,Object 类是所有类的基类
  • 父类构造器的调用不限于直接父类,将一直往上追溯,直到 Object 类 (顶级父类)
  • 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。

如果要让 A 继承 B 和 C,得先 A 继承 B,B 继承 C

  • 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
1
2
3
Cat extends Animal//合理

Person extends Music//不合理

# super 关键字

用 super 调用父类的构造器,可使分工明确,父类属性由父类初始化,子类的属性由子类初始化

# 语法

  • 访问父类的属性,但不能访问父类的 private 属性 super. 属性名
1
System.out.println(super.n1+super.n2);
  • 访问父类的方法,不能访问父类的 private 方法 super. 方法名 ()
1
super.test();
  • 访问父类的构造器 super (参数列表);只能放在构造器的第一句
1
2
3
super("jack");
super();
super("jack, 22");

# 细节

  • 当子类中有和父类中的成员重名时,为了访问父类的成员,必须通过 super,如果没有重名,使用 super、this、直接访问时一样的效果
  • super 的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用 super 去访问爷爷类的成员;如果多个基类中都有同名的成员,使用 super 访问遵循就近原则。

# 方法重写 / 覆盖 (override)

方法覆盖就是子类有一个方法和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法 +

** 作用:** 可以达到不更改父类的方法便创建了子类的一个方法。使程序更加的安全

# 注意事项和使用细节

  • 子类的方法的参数,方法名称,要和父类方法的参数名称完全一样
  • 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类,比如父类返回类型是 object,子类返回类型是 String
  • 子类方法不能缩小父类方法的访问权限

# 多态

问题引出:一个主人有几个宠物,比如小狗和猫,小狗吃骨头,猫吃鱼,可能还会吃别的东西,那么在这种情况下,我们要建立一个小狗的类、小猫的类,还有食物的类,而食物又分为骨头,鱼,后面可能还会有别的,那么这些食物就是 food 的子类。在这种情况下,我每多一个宠物,我就要在主方法中多写一个方法来调用他们,这样会随着项目的扩展导致方法数量太多,不利于管理和维护

而在多态的情况下,父类对象可以指向子类对象,那么只要是这个父类的子类,都可以直接传入同一个方法中来调用,根据子类的不同而输出不同的值

# 方法的多态

方法的重写和重载就体现了多态

# 对象的多态

  1. 一个对象的编译类型和运行类型可以不一致
  2. 编译类型在定义对象时就确定了,不能改变
  3. 运行类型是可以变化的
  4. 编译类型看定义时 “=” 号的左边,运行类型看 “=” 号的右边
1
2
Animal animal = new Dog();//animal编译类型是Animal,运行类型是Dog
animal = new Cat();//animal的运行类型变成了Cat,编译类型仍然是Animal

# 注意事项

  • 多态的前提是两个对象(类)存在继承关系
  • 属性没有重写之说,属性的值看编译类型。(如果转型的类中有和父类一样的属性,那么就看编译的是哪个类,值就是哪个类的值)
  • instanceof 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的(前面这个是否为后面这个的)子类型
1
2
3
4
5
6
7
8
9
10
11
12
//编译类型运行类型都为BB,但BB是AA的子类,所以返回结果都为ture
BB bb = new BB();
System.out.println(bb instanceof BB);
System.out.println(bb instanceof AA);

//aa编译类型AA,运行类型BB
AA aa = new BB();
System.out.println(aa instanceof AA);
System.out.println(aa instanceof BB);
//返回结果都为ture
class AA{}
class BB exetnds AA{}

# 多态向上转型

本质:父类的引用指向了子类的对象(也就是向上转型)。只要是继承关系,父类都可以指向下面的任意子类

特点:编译类型看左边,运行类型看右边

语法:父类类型 应用名 = new 子类类型 ();

1
Animal animal = new Cat();
  • 可以调用父类中的所有成员(需遵守访问权限,比如私有就无法调用)
  • 不能调用子类中的特有成员(父类中没有的,就不能调用)
  • 最终的运行效果看子类的具体实现

# 向下转型

为了代码维护的便利,我们利用多态的性质进行了一个向上转型,但当我们在向上转型之后,又需要用到子类中的成员,但此时无法引用,这还是就可以向下转型进行一个强转,强行把指向父类又改成指向子类,这样就可以使用子类的成员了

语法:子类类型 引用名 = (子类类型)父类引用;

1
Cat cat = (Cat) animal;
  • 只能强转父类的引用,不能强转父类的对象
  • 要求父类引用必须指向的是当前目标类型的对象(就是如果要向下转型,你转型的目标原本就是这个类型的,只不过之前向上转型了,现在相当于是复原回它原本的类型)
  • 可以调用子类类型中所有的成员

# java 动态绑定机制

  1. 当调用对象方法的时候,该方法回合该对象的内存地址 (也就是运行类型) 绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明就用哪里的

# 多态数组

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

1
2
3
4
5
//Student和Teacher都是Person的子类,数组为Person类,里面的数据可以存放子类
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("tch", 21);
persons[2] = new Teacher("king", 30, 20000);

# 多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型

1
2
3
4
5
6
Student tch = new Student("tch", 21);
Teacher jack = new Teacher("jack", 35);
//这里Student和Teacher都是Person的子类,可以直接将这样一个对象传给AAA的形参
public void AAA(Person a) {
System.out.println(a.say());
}

# Object 类

# ==

  1. == 既可以判断基本类型,又可以判断引用类型
  2. == 如果判断基本类型,判断的是值是否相等
  3. == 如果判断引用类型,判断的是地址是否相等,即判定是不是同一个对象
1
2
3
A a = new A();
A b = a;
A c = b;

# equals()

equals 是 Object 类中的方法,只能判断引用类型,默认判断的是地址是否相等,子类中往往要重写该方法,用于判断内容是否相等,比如 Integer,String,这两个之所以可以直接比较值,是因为 integer 类和 String 类中已经重写了 equals 方法

如果没有重写 equals 方法,那么就是比较地址,如果重写了,就是比较重写的值,根据这个我们可以自己重写我们所需要的 equals 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
private String name;
private int age;
private char gender;

//重写Object的equals方法
public boolean equals(Object obj) {
//判断如果比较的两个对象是同一个对象,返回ture
if(this == obj) {
return true;
}
//类型判断
if(obj instanceof Person) {//是Person才开始比较,否则直接返回false
//进行类型转换,现在相当于是Object类,所以需要向下转型,我需要得到obj的各个属性来进行判断
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
} else {
return false;
}
}

# hashCode 方法

返回该对象的哈希码值,支持此方法是为了提高哈希表的性能。

  1. 提高具有哈希结构的容器的效率
  2. 两个引用,如果一个指向的是同一个对象,哈希值一样
  3. 如果指向的是不同的对象,哈希值不一样
  4. 哈希值主要根据地址号计算,不能完全将哈希值等价于地址
  5. 后面在集合中,hashCode 如果需要的话,也可以重写

# toString 方法

  • 默认返回:全类名 (包名 + 类名)+@+ 哈希值的 16 进制,子类往往重写 toString 方法,用于返回对象的属性信息
  • 重写 toString 方法,打印对象或拼接对象时,都会自动调用该对象的 toString 形式
  • 当直接输出一个对象时,toString 方法会被默认的调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//重写toString方法输出对象的属性,使用快捷键alt+insert -> toStirng
class Monster {
private String name;
private int age;
private String job;

public Monster(String name, int age, String job) {
this.name = name;
this.age = age;
this.job = job;
}
//重写后一般是把对象的属性值输出,也可以自己定制
@Override
public String toString() {
return "Monster{" +
"name='" + name + '\'' +
", age=" + age +
", job='" + job + '\'' +
'}';
}
}

# finalize 方法

这个实际开发中几乎不会用,应付面试

  1. 当对象被回收时,系统自动调用该对象的 finalize 方法,释放资源,子类可重写该方法
  2. 什么时候被回收:当某个对象没有任何引用时,jvm 会认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象之前,会先调用 finalize 方法
  3. 垃圾回收机制的调用,是由系统来决定 (即有自己的 GC 算法),也可以通过 System.gc () 主动触发垃圾回收机制

# 断点调试

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

# 快捷键

F7(跳入) shift+F8(跳出) F9(resume,执行到下一个断点)

F7:跳入方法内 F8:逐行执行代码 shift+F8:跳出方法

# 类变量和类方法

类变量和类方法也叫做静态变量和静态方法,可以直接使用而不需要初始化,对所有对象都共享。

  • 在 java7 以上,静态变量和静态方法都是储存在方法区;java7 及以下的静态变量储存在常量池的一个专门储存 static 变量的地方
  • 类方法中不能使用和对象有关的关键字,比如 this 和 super
  • 静态方法只能访问静态变量或方法
  • 静态方法不能被重写

# main 方法语法

  • main 方法接受 String 类型的数组参数,该数组中保存执行 java 命令时传递给所运行的类的参数
1
2
3
4
5
6
7
8
9
10
//语法:java 执行的程序 参数1 参数2 参数3
public class hello {
public static void main(String[] args) {
for(int i = 0; i<args.length; i++) {
System.out.println(args[i]);
}
}
}
//在cmd中执行这个程序,然后输入“java hello tch tchh tchhh”,就会输出这几个字符串

# 代码块

代码块又称为初始化块,属于类中的成员,是类的一部分,类似于方法,将逻辑语句封装在方法体中,用 {} 包围。但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显示调用,而是加载类时,或创建对象时隐式调用。

1
2
3
4
//基本语法
[修饰符]{
//代码
};
  • 修饰符可选,要写的话也只能写 static
  • 代码块分为两类,使用 static 修饰的交静态代码块,没有 static 修饰的交普通代码块
  • 逻辑语句可分为任何逻辑语句(输入输出、方法调用、循环、判断等)
  • 分号可以省略

# 代码块的好处

  • 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
  • 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的复用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Movie{
private String title;
private String director;
private int year;
/**
* 这里有三种构造器,而我们在构造器前添加了普通代码块,注意是普通代码块
* 那么无论是哪个构造器被调用,都会先执行代码块中的内容,这样就提升了代码的复用性
* 就不需要再每个构造器中都写上这三句代码
*/
{
System.out.println("映前广告");
System.out.println("广告结束");
System.out.println("电影开始");
};

public Movie(String title) {
this.title = title;
}

public Movie(String title, String director) {
this.title = title;
this.director = director;
}

public Movie(String title, int year, String director) {
this.title = title;
this.year = year;
this.director = director;
}
}

# 细节

  • static 代码块的作用是对类进行初始化,而且随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象,就执行一次

类什么时候被加载:

  1. 创建实例时(new)
  2. 创建子类对象实例,父类也会被加载,父类先加载,子类后加载
  3. 使用类的静态成员时(静态变量 / 方法)
  • 普通代码块在创建对象实例时会被隐式的调用,被创建一次就会被调用一次。但如果只是使用类的静态成员,普通代码块不会执行

  • 创建对象时,代码块在类中的调用顺序如下 1->2->3

    1. 调用静态代码块和静态属性初始化(静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
    2. 调用普通代码块和普通属性的初始化(普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和普通属性初始化,则按定义顺序调用)
    3. 调用构造方法
  • 构造器的最前面其实隐含了 super () 和调用普通代码块的一句话,静态相关的代码块,属性初始化,在类加载时就执行完毕,因此是优先于构造器和普通代码块执行的

1
2
3
4
5
6
7
8
class A {
public A() {
//隐藏语句:
//(1)super();
//(2)调用普通代码块的语句
System.out.println("ok");
}
}
  • 创建一个子类对象时的加载顺序:(面试高频)
    1. 父类的静态代码块和静态属性
    2. 子类的静态代码块和静态属性
    3. 父类的普通代码块和普通属性初始化
    4. 父类的构造方法
    5. 子类的普通代码块和普通属性初始化
    6. 子类的构造方法

# 单例设计模式

单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。单例模式有两种方式:饿汉式和懒汉式

# 饿汉式

无论是否使用对象,都会先创建,所以叫饿汉式。但这样有可能会造成资源浪费

步骤如下

  1. 构造器私有化(防止直接 new
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法 getInstance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SingleTon {
public static void main(String[] args) {
GirlFriend girlFriend = GirlFriend.getGirlFriend();
System.out.println(girlFriend);//无论创建几次新的对象,都是指向GirlFriend内部创建的对象
}
}

class GirlFriend {
private String name;
//为了能够在静态方法中使用,需要将其静态化
private static GirlFriend gf = new GirlFriend("xiaohong");
//如何保证只能创建一个GirlFriend对象
//1.将构造器私有化
//2.在类的内部直接创建
//3.提供一个公共的static方法,返回gf对象
private GirlFriend(String name) {
this.name = name;
}
public static GirlFriend getGirlFriend() {
return gf;
}

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

# 懒汉式

在需要使用的时候才创建对象,所以叫懒汉式

步骤如下:

  1. 构造器私有化
  2. 定义一个 static 对象
  3. 提供一个 public 方法,可以返回 Cat 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SingleTon2 {
public static void main(String[] args) {
Cat mao = Cat.getInstance();
System.out.println(mao);
}

}

//希望在程序运行过程中只能创建一个Cat对象
class Cat {
private String name;
private static Cat cat;
//1.构造器私有化
//2.定义一个static对象
//3.提供一个public方法,可以返回Cat对象
private Cat(String name) {
this.name = name;
}

public static Cat getInstance() {
if (cat == null) {//如果没有创建Cat对象
cat = new Cat("mimi");
}
return cat;
}

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

# 两者区别

  1. 最主要的区别在于创建对象的时机不同,饿汉式是在类加载就创建了对象实例,而懒汉式是在使用是才创建
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题
  3. 饿汉式存在浪费资源的可能,懒汉式因为是使用时才创建,不存在这个问题
  4. 在 javaSE 标准类中,java.lang.Runtime 就是经典的单例模式

# final 关键字

final 可以修饰类、属性、方法和局部变量,在什么时候会用到 final 关键字

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

# 注意事项

  • final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名
1
public final double TAX_RATE = 0.08;
  • final 修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在以下位置之一
    1. 定义时:如 public final double TAX_RATE = 0.08;
    2. 在构造器中
    3. 在代码块中
  • 如果 final 修饰的属性是静态的,则初始化的位置只能是:定义时;在静态代码块;不能在构造器中赋值
  • final 类不能继承,但可以实例化对象
  • 如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承
  • 一般来说,如果一个类已经是 final 类了,就没必要将方法再修饰成 final
  • final 不能修饰构造器
  • final 和 static 往往搭配使用,效率更高,底层编译器做了优化处理,不会导致类加载
  • 包装类(integer,double,float,boolean 等都是 final),String 也是 final 类

# 抽象类

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

一般情况都是让子类继承来实现方法,抽象类的价值更多是在于设计,设计者设计好之后,让子类继承并实现抽象类。抽象类是考官比较爱问的知识点,在框架和设计模式使用较多

1
2
3
4
5
6
7
8
9
10
//当一个类中存在抽象方法时,类也要声明为抽象类
abstract class Animal {
private String name;

public Animal(String name) {
this.name = name;
}

public abstract void eat();
}

# 细节

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

# 抽象模板模式

在模板模式中,一个抽象类公开定义了执行它的方法的方式,它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,这种类型的设计模式属于行为型模式。

# 主要解决的问题

解决在多个子类中重复实现相同的方法的问题,通过将通用方法抽象到父类中来避免代码重复

** 使用建议:** 当有多个子类共有的方法且逻辑相同时,考虑使用模板方法定义在父类中;对于重要或复杂的方法,可以考虑作为模板方法定义在父类中

# 优缺点

  • ** 封装不变部分:** 算法的不变部分被封装在父类中
  • ** 扩展可变部分:** 子类可以扩展或修改算法的可变部分
  • ** 提取公共代码:** 减少代码重复,便于维护

缺点就是 ** 类数目增加:** 没个不同的实现都需要一个子类,可能倒是系统庞大

# 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建一个抽象类,它的模板方法被设置为final
public abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();

//模板
public final void play(){
//初始化游戏
initialize();
//开始游戏
startPlay
//结束游戏
endPlay();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//创建扩展了上述类的实体类
public class Cricket extends Game {

@Override
void endPlay() {
System.out.println("Cricket Game Finished!");
}
@Override
void initialize() {
System.out.println("Cricket Game Initialized! Start playing.");
}
@Override
void startPlay() {
System.out.println("Cricket Game Started. Enjoy the game!");
}
}

public class Football extends Game {

@Override
void endPlay() {
System.out.println("Football Game Finished!");
}
@Override
void initialize() {
System.out.println("Football Game Initialized! Start playing.");

@Override
void startPlay() {
System.out.println("Football Game Started. Enjoy the game!");
}
}

# 接口

接口就是给出一些没有实现的方法,到某个类要使用的时候,再根据具体情况把这些方法写出来

1
2
3
4
5
6
7
8
9
interface 接口名{
//属性
//方法(抽象方法/默认方法/静态方法)
}
class 类名 implements 接口{
自己属性;
自己方法
必须实现的接口的抽象方法
}
  • 在 jdk7.0 前,接口里的所有方法都没有方法体
  • jdk8.0 后接口类可以有静态方法和默认方法 (static 和 default),也就是说接口中可以有方法的具体实现
1
2
3
4
5
/* 文件名 : Animal.java */
interface Animal {
public void eat();
public void travel();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{

public void eat(){
System.out.println("Mammal eats");
}

public void travel(){
System.out.println("Mammal travels");
}

public int noOfLegs(){
return 0;
}

public static void main(String args[]){
MammalInt m = new MammalInt();
m.eat();
m.travel();
}
}

# 细节

  1. 接口不能被实例化
  2. 接口中所有的方法都是 public 方法,如果不写也是默认 public,接口中抽象方法可以不用 abstract 修饰
  3. 一个普通类实现接口,就必须将该接口的所有方法都实现(alt+enter 快捷键)
  4. 抽象类实现接口,可以不用实现接口的方法
  5. 一个类可以同时实现多个接口
  6. 接口中的属性只能是 final 类型,而且是 public static final 修饰符,但是被隐藏了,比如 int a=1; 实际上是 public static final int a=1;(必须初始化)
  7. 接口中书信固定访问形式:接口名。属性名
  8. 一个接口不能继承其他的类,但是可以继承多个别的接口:interface A extends B,C {}
  9. 接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的

# 接口和继承的比较

当子类继承了父类,就自动拥有了父类的功能。如果子类需要扩展功能,可以通过实现接口的方式来扩展。

可以理解为,实现接口是对 java 单继承机制的一种补充。

  • 继承的作用主要在于:解决代码的复用性和可维护性

  • 而接口的作用在于:设计。设计好各种规范,让类来实现这些方法

  • 接口比继承更加灵活,继承是满足 is - a 的关系,而接口秩序满足 like - a 的关系

  • 接口在一定程度上实现代码解耦

# 接口的多态

  • 多态参数
1
2
3
//实现了这个接口的类的对象,都可以作为形参传递
public void work(UsbInterface a) {//这里a就是接口类型的类的对象实例
}
  • 多态数组
1
2
3
4
5
6
7
8
9
10
public class AAA{
public static void main(String[] args){
Usb[] usb = new Usb[2];
usb[0] = new Phone();
usb[1] = new Camera();
}
}
interface Usb{}
class Phone implements Usb{}
class camera implements Usb{}
  • 接口存在多态传递现象
1
2
3
4
5
6
7
8
9
public class AA{
public static void main(String[] args){
IG ig = new Teacher();
TH ih = new Teacher();
}
}
interface IH {}
interface IG extends IH{}
class Teacher implements IG {}

# 内部类

一个类的内部又完整的嵌套类另一个类结构。被嵌套的类称为内部类 (inner class),嵌套其他类的类称为外部类 (outer class),是类的第五大成员 (属性、方法、构造器、代码块、内部类),内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

1
2
3
4
5
class Outer{//外部类
class Inner{//内部类
}
}
class Other{}//其他类

# 内部类的分类

  • 定义在外部类局部位置上(比如方法内):
    1. 局部内部类(有类名)
    2. 匿名内部类(没有类名,重点)
  • 定义在外部类的成员位置上:
    1. 成员内部类(没用 static 修饰)
    2. 静态内部类(使用 static 修饰)

# 局部内部类的使用

局部内部类是定义在外部类的局部位置,比如方法中,并且有类名

  1. 局部内部类可以直接访问外部类的所有成员,包括私有成员

  2. 不能添加访问修饰符,因为他的低位就是应该局部变量,局部变量是不能使用修饰符的,但是可以使用 final 修饰,因为局部变量也可以使用 final

  3. 作用域:只在定义它的方法或代码中

  4. 局部内部类访问外部类的成员,可以直接访问

  5. 外部类访问局部类的成员,需要创建对象再访问

  6. 外部其他类不能访问局部内部类,因为局部内部类地位是一个局部变量

  7. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this. 成员)去访问

    • System.out.println("外部类的n=" + 外部类名.this.n);
      <!--code59-->

匿名内部类是系统自动生成的,表面上以一个对象的形式存在,但实际上底层生成了一个类,比如外部类叫做 Outer,那么匿名内部类会叫做 Outer$1,如果后面还有匿名内部类就依次按顺序排号

1
2
3
4
5
6
7
8
9
10
11
12
13
class Outer {
new Person(){
@Override
public void hi() {
System.out.println("hi");
}
};
}
class Person {
public void hi(){
System.out.println("hi~~~");
}
}

实际上他的形式在底层是这样的

1
class Outer$1 extends Person {}

# 细节

  • 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时本身也是一个对象,因此从语法上看,它既有定义类的特征也有创建对象的特征。所以可以直接调用匿名内部类方法

如何调用呢,以上面的为例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Outer {
new Person(){
@Override
public void hi() {
System.out.println("hi");
}
}.ok();//可以直接在后面跟着调用,输出hi
}
class Person {
public void hi(){
System.out.println("hi~~~");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Outer {
Person p = new Person(){
@Override
public void hi() {
System.out.println("hi");
}
};
p.hi();//动态绑定,运行类型是Outer$1,所以输出的是hi而不是hi~~~
}
class Person {
public void hi(){
System.out.println("hi~~~");
}
}
  • 可以直接访问外部类的所有成员,包含私有的
  • 不能添加访问修饰符,因为它的地位是一个局部变量
  • 作用域:仅仅在定义它的方法或代码块中
  • 匿名内部类访问外部类成员,直接访问
  • 外部其他类不能访问匿名内部类,因为匿名内部类相当于一个局部变量
  • 如果外部类和内部类的成员重名,在内部类访问时,遵循就近原则,如果想访问外部类的成员,可以使用(外部类名.this. 成员)去访问

# 实例

当作实参直接传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Grass {
public static void main(String[] args) {

//当作实参直接传递
f1(new IL() {
@Override
public void show() {
System.out.println("hi");
}
});
}
//静态方法,形参是接口类型
public static void f1(IL il) {
il.show();
}
}

//接口
interface IL{
void show();
}

对比一下传统的方法(硬编码),如果说要多次使用,可以就用传统方法,但如果只用一次,直接传递会更简洁高效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Grass {
public static void main(String[] args) {
//在这调用
f1(new Picture());
}
//静态方法,形参是接口类型
public static void f1(IL il) {
il.show();
}
}

//接口
interface IL{
void show();
}

class Picture implements IL{
@Override
public void show() {
System.out.println("picture");
}
}

有一个铃声接口 Bell,里面有一个 ring 方法;有一个手机类 Cellphone,具有闹钟功能 alarmclock,参数是 Bell 类型;测试手机类的闹钟功能,通过匿名内部类作为参数,输出 "起床啦";再传入一个匿名内部类对象,输出 "get up!"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Grass {
public static void main(String[] args) {

Cellphone cellphone = new Cellphone();
cellphone.alarmclock(new Bell() {
@Override
public void ring() {
System.out.println("起床啦");
}
});
cellphone.alarmclock(new Bell() {
@Override
public void ring() {
System.out.println("get up!");
}
});
}

}


class Cellphone{
public void alarmclock(Bell bell) {
bell.ring();
}
}

interface Bell{
void ring();
}

# 成员内部类的使用

成员内部类是定义在外部类的成员位置,并没有 static 修饰

  1. 可以直接访问外部类的所有成员,包括私有的

  2. 可以添加任意访问修饰符(public、protected、默认、private),因为它本身也是一个成员

  3. 作用域和外部类的其他成员一样为整个类体,在外部类的成员方法中创建成员内部类对象,再调用方法

  4. 成员内部类访问外部类,直接访问

  5. 外部类访问成员内部类,创建对象再访问

  6. 外部其他类访问成员内部类

    1. // 第一种方式,相当于把 new Inner () 当作是 Outer 的成员
      Outer.Inner inner = Outer.new Inner();
      <!--code67-->
    2. // 第三种其实本质上就是前两种的结合
      new Outer().new Inner();
      <!--code68-->
  7. 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员

  8. 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员

  9. 作用域:同其他的成员,为整个类体

  10. 静态内部类访问外部类,可以直接访问所有静态成员

  11. 外部类访问静态内部类,创建对象再访问

  12. 外部其他类访问静态内部类

    1
    Outer.Inner inner = new Outer.Inner();
  13. 如果外部类和内部类的成员重名,在内部类访问时,遵循就近原则,如果想访问外部类的成员,可以使用(外部类名。成员)去访问

# 枚举类

枚举是一组常量的集合,属于一种特殊的类,里面只包含一组有限的特定的对象

  1. 不需要提供 setXxx 方法,因为枚举对象值通常为只读
  2. 对枚举对象 / 属性使用 final+static 共同修饰,实现底层优化
  3. 枚举对象名通常使用全部大写,常量的命名规范
  4. 枚举对象根据需要,也可以有多个属性

# 实例

  1. 用自定义类来实现枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class Grass {
    public static void main(String[] args) {
    System.out.println(Season.SPRING);
    System.out.println(Season.SUMMER);
    System.out.println(Season.AUTUMN);
    System.out.println(Season.WINTER);
    }
    }

    class Season {
    private String name;
    private String desc;

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


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

    public String getName() {
    return name;
    }
    public String getDesc() {
    return desc;
    }

    @Override
    public String toString() {
    return "Season{" +
    "name='" + name + '\'' +
    ", desc='" + desc + '\'' +
    '}';
    }
    }
  2. 使用 enum 关键字实现枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class Grass2 {
    public static void main(String[] args) {
    System.out.println(Season.SPRING);
    System.out.println(Season.SUMMER);
    System.out.println(Season.AUTUMN);
    System.out.println(Season.WINTER);
    }
    }

    enum Season2 {

    //使用关键字enum代替class
    //如果有多个常量(对象),用逗号间隔
    //格式:常量名(实参列表)
    //如果使用enum来实现枚举,要求将定义常量对象写在最前面
    SPRING("春天", "温暖"),SUMMER("夏天", "炎热"),
    AUTUMN("秋天", "凉爽"),WINTER("冬天", "寒冷"),WHAT;
    private String name;
    private String desc;

    private Season2{}//无参构造器,上面的WHAT因为没有传入参数所以是调用的无参构造器

    private Season2(String name, String desc) {
    this.name = name;
    this.desc = desc;
    }

    public String getName() {
    return name;
    }
    public String getDesc() {
    return desc;
    }

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

# 细节

  1. 当使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类,且是一个 final 类(可以通过反编译看到)
  2. 传统的 public static final Season SPRING = new Season (“春天”, “温暖”); 简化成 SPRING (“春天”, “温暖”),这里必须知道它调用的时哪个构造器
  3. 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
  4. 当有多个枚举对象时,使用逗号间隔,分号结尾
  5. 枚举对象必须放在枚举类的行首

# enum 常用方法

因为 enum 会默认继承 Enum 类,在父类中有些方法可以用

  1. toString:Enum 类已经重写过了,返回的是当前对象的属性信息
  2. name:返回当前对象名(常量名),子类中不能重写
  3. ordinal:返回当前对象的位置号,默认从 0 开始
  4. values:返回当前枚举类中所有的常量
  5. valueOf:将字符串转换成枚举对象,要求字符串必须为已有的常量名,否则报异常
  6. compareTo:比较两个枚举常量,比较的就是位置号。(位置编号相减得 0 为真)

# enum 实现接口

  1. 使用 enum 关键字后,不能再继承其他类,因为 enum 类会隐式继承 Enum,而 java 是单继承机制

  2. 枚举类和普通类一样,可以实现接口

    1
    enum 类名 implements 接口1, 接口2{}

# 注解

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

使用 Annotation 时要在其前面加 @符号,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素

三个基本的 Annotation:

  1. @Override:限定某个方法,是重写父类方法,该注解只能用于方法
  2. @Deprecated:用于表示某个程序元素(类、方法等)已过时
  3. @SuppressWarnings:抑制编译器的警告

# @Override

  1. @Override 只能修饰方法,不能修饰其他类、包、属性等
  2. @Target 是修饰注解的注解,称为元注解
  3. 查看 @Override 注解的源码为 @Target (ElementType.METHOD),说明只能修饰方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father{
public void fly() {
System.out.println("Father fly");
}
}
//这个@Override其实是可写可不写,但是写了会让系统再编译的时候去检查这里是否真的重写了,重写了才能编译成功,没重写就会报错
class Son extends Father {
@Override
public void fly() {
System.out.println("Son fly");
}
}

public @interface Override {
//这是一个注解类,不是接口
}

# @Deprecated

  1. 用于表示某个程序元素(类、方法、属性等)已过时
  2. 可以修饰方法、类、字段、包、参数等
  3. @Deprecated 的作用可以做到新旧版本的兼容和过渡
  4. @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Grass2 {
public static void main(String[] args) {
A a = new A();
a.hi();
System.out.println(a.n);
}
}
@Deprecated
class A {
@Deprecated
public int n = 1;
@Deprecated
public void hi() {
System.out.println("hi");
}
}

# @SuppressWarnings

有时候写代码,代码其实可以正常运行,但是会有一些警告,这个时候就可以用 @SuppressWarnings 注解来抑制警告信息

1
2
@SuppressWarnings({"all", "rawtypes"})
//除了all还有专门抑制其他某种类型警告,具体的使用可以查阅资料
  • @SuppressWarnings 的作用范围和放置的位置相关,比如放在 main 方法,那么抑制的范围就是 main 方法

# 异常 Exception

快捷键:ctrl + alt + t 选择 try-catch

# 异常的概念

一般可以分为两类

  1. Error:java 虚拟机无法解决的严重问题,如:JVM 系统内部错误、资源耗尽等严重情况。比如 StackOverflow [栈溢出] 和 OOM (out of memory),Error 是严重错误,程序会崩溃
  2. Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如空指针方向,试图读取不存在的文件,网络连接中断等等,Exception 分为两大类:运行时异常和编译时异常

# 异常体系图

image-20240908205218302

  1. 异常分为两大类,运行时异常和编译时异常
  2. 运行时异常编译器检查不出来,一般是指编程时的逻辑错误,时程序员应该避免其出现的异常。java.lang.RuntimeException 类以及它的子类都是运行时异常
  3. 对于运行时异常,可以不做处理,因为这类异常很普遍,若全处理看你会对程序的可读性和运行效率产生影响
  4. 编译时异常时编译器要求必须处理的异常
  • 常见的运行时异常:
    1. NullPointException 空指针异常:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
    2. ArithmeticException 数学运算异常
    3. ArrayIndexOutOfBoundsException 数组下标越界异常
    4. ClassCastException 类型转换异常
    5. NumberFormatException 数字格式不正确异常
  • 常见的编译异常:
    1. SQLException:操作数据库时,查询表可能发生异常
    2. IOException:操作文件时发生的异常
    3. FileNotFoundException:当操作一个不存在的文件时发生异常
    4. ClassNotFoundException:加载类,而该类不存在时异常
    5. EOFException:操作文件,到文件末尾,发生异常
    6. IllegalArgumentException:参数异常

# 异常处理概念

异常处理就是当异常发生时,对异常处理的方式

# try-catch-finally

程序员在代码中捕获发生的异常,自行处理

try 块用于包含可能出错的代码,catch 块用于处理 try 块中发生的异常,可以根据需要在程序中有多个 try-catch 模块

注意事项:

  1. 如果异常发生了,则异常发生后面的代码都不会执行,直接进入到 catch 块
  2. 如果异常没有发生,则顺序执行 try 的代码块,不会进入 catch
  3. 如果希望不管是否发生异常,都执行某段代码(比如关闭连接,释放资源等),则使用 finally {}
  4. 如果 try 代码块可能有多个异常,,可以使用多个 catch 分别捕获不同的异常,但是要求子类异常写在前面,父类一场写在后面(比如 Exception 在后,NullPointerException 在前),如果发生异常,只会匹配一个 catch
  5. 可以进场 try-finally 配合使用,这种用法相当于没有捕获异常,因此程序会直接崩掉。当希望执行一段代码,不管是否发生异常,都必须执行某个业务逻辑时可以使用
1
2
3
4
5
6
7
8
9
10
try {
//可疑代码
//将异常生成对应的异常对象,传送给catch块
}catch(异常) {
//对异常的处理
}catch(异常) {
//对异常的处理
}finally {
//释放资源等
}

# throws

将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是 JVM

如果一个方法(中的语句执行时)可能生成某种异常,但不能确定如何处理这种异常,则此方法显式地声明抛出异常,表明该方法将不对这些异常进行处理,而又该方法的调用者处理

在方法声明中用 throws 语句可以声明抛出异常的列表,throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类

注意事项:

  1. 对于编译异常,程序中必须处理,比如 try-catch 或者 throws
  2. 对于运行时异常,程序中如果没有处理,默认就是 throws 的方式处理
  3. 子类重写父类的方法时,对抛出异常的规定:子类重写的方法所抛出的异常类型要么和父类抛出的一致,要么为父类抛出的异常的类型的子类型
  4. 在 throws 过程中,如果有方法 try-catch,就相当于处理异常,就可以不必 throws
1
2
3
4
public void f1() throws Exception {//这里是Exception是父类,也可以是FileNotFoundException子类
//也可以多个子类异常,逗号分隔
FileInputStream fis = new FileInputStream("d://1.txt");
}

# 自定义异常

当程序中出现了某些错误,但该错误信息并没有在 Throwable 子类中描述处理,这个时候可以自己设计异常类,用于描述该错误信息

自定义异常的步骤

  1. 定义类:自定义异常类名(程序员自己写),继承 Exception 或 RuntimeException
  2. 如果继承 Exception,属于编译异常
  3. 如果继承 RuntimeException,属于运行异常(一般来说继承 RuntimeException)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Grass {
public static void main(String[] args) {
int age = 151;
if(!(age >= 18 && age <= 120)){
throw new AgeException("age should between 18 and 120.");
}
}
}
//一般情况我们自定义异常时,最好是继承RuntimeException
//这样比较方便,不需要在主方法中再去抛异常
class AgeException extends RuntimeException {
public AgeException(String message) {//构造器
super(message);
}
}

# throw 和 throws 的对比

意义 位置 后面跟的东西
throws 异常处理的一种方式 方法声明处 异常类型
throw 手动生成异常对象的关键字 方法体中 异常对象

# 常用类

# 包装类 Wrapper

# 包装类的分类

  1. 针对八种基本数据类型相应的应用类型 —— 包装类
  2. 有了类的特点,就可以调用类中的方法
基本数据类型 包装类
boolean Boolean
char Character
byte Byte
short Short
int Integer
long Long
float Float
double Double

# 包装类和基本数据类型的转换

包装类是对象,基本数据类型是属性

  1. jdk5 前的手动装箱和拆箱方式,装箱:基本类型 -> 包装类型,反过来就是拆箱
  2. jdk5 及以后的自动装箱和拆箱方式
  3. 自动装箱底层调用的时 valueOf 方法,比如 Integer.valueOf ()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Grass {
public static void main(String[] args) {
//手动装箱int->Integer
int n1 = 100;
Integer integer = new Integer(n1);
Integer integer1 = Integer.valueOf(n1);

//手动拆箱Integer->int
int i = integer.intValue();

//jdk5后,就可以自动装箱和自动拆箱
int n2 = 200;
Integer interger2 = n2;//底层使用的依然还是Integer.valueOf(n2)
//自动拆箱
int n3 = interger2;//底层使用的还是intValue()方法
}
}

# 包装类型和 String 类型的相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Grass {
public static void main(String[] args) {
//包装类(Integer)->String
Integer i = 100;
//way 1
String str = i + "";//这里只是以i为主体进行了一个转换,但i自己本身还是没变化
//way2
String str2 = i.toString();
//way3
String str3 = String.valueOf(i);

//String->包装类(Integer)
String str4 = "12345";
//way1
int i2 = Integer.parseInt(str4);//使用到了自动装箱
//way2
Integer i3 = new Integer(str4);//用到了构造器,可以ctrl+b查看
}
}

# Integer 类和 Character 类的常用方法

这几个是比较常用的,还有非常多

1
2
3
4
5
6
7
8
9
10
11
System.out.println(Integer.MIN_VALUE);//返回最大/最小值
System.out.println(Integer.MAX_VALUE);

System.out.println(Character.isDigit('a'));//判断是不是数字
System.out.println(Character.isLetter('a'));//判断是不是字母
System.out.println(Character.isLowerCase('a'));//判断大小写
System.out.println(Character.isUpperCase('a'));

System.out.println(Character.isWhitespace('a'));//判断是不是空格
System.out.println(Character.toUpperCase('a'));//转换大小写
System.out.println(Character.toLowerCase('A'));

# Integer 类

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//由于生成的是两个对象,所以结果为false
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);

Integer m = 1;//底层为Integer.valueOf(1);
Integer n = 1;//底层为Integer.valueOf(1);
System.out.println(m == n);//在valueOf源码中,赋值在-127~128之间的,
//就直接返回数值,而超过这个范围的,就new一个对象返回,所以这里为true

Integer x = 128;
Integer y = 128;
System.out.println(x == y);
//而这里大于了127不在范围中,返回一个对象,所以x和y是不同的两个对象,为false
1
2
3
4
5
6
Integer x = 128;
int y = 128;
System.out.println(x == y);
//这个要注意,只要有int类型的(基本数据类型),那么做比较的时候就是比较值是否相等
//哪怕x的底层是new一个对象,但这里对比的还是值的大小
//所以这里输出为true

# String 类

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

  2. String 类较常用构造器

    1
    2
    3
    4
    String s1 = new String();
    String s2 = new String(String original);
    String s3 = new String(char[] a);
    String s4 = new String(char[] a, int startIndex, int count);
  3. String 类实现了接口 Serializable(String 可以序列化,在网络传输),还实现了 Comparable(对象可以比较大小)

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

  5. String 有属性 private final char value []; 用于存放字符串内容,所以其本质是字符数组

  6. value 是一个 final 类型,不可以修改(地址不可修改,值可以修改)

# 创建 String 对象的两种方式

  1. 直接赋值: String s = "tch";

    创建过程:先从常量池查看是否有 "tch" 数据空间,如果有,则直接指向它;如果没有则重写创建,然后指向。s 最终指向的是常量池的空间地址

  2. 调用构造器: String s = new String("tch");

    创建过程:先在堆中创建空间,里面维护了 value 属性,指向常量池的 "tch" 空间。如果常量池没有 "tch",就重新创建,如果有,就直接通过 value 指向。最终指向的是堆中的空间地址

    # 实例

1
2
3
4
5
6
7
8
String a = "tch";
String b = new String("tch");

System.out.println(a.equals(b));//T
System.out.println(a == b);//F
System.out.println(a == b.intern());//T
System.out.println(b == b.intern());//F
//intern()方法的作用:如果池中已经包含了一个等于这个String对象的字符串,就直接返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用
1
2
3
4
5
6
7
8
9
10
11
12
Person p1 = new Person();
p1.name = "tch";
Person p2 = new Person();
p2.name = "tch";

System.out.println(p1.name.equals(p2.name));//T
System.out.println(p1.name == p2.name);//T
System.out.println(p1.name == "tch");//T

String s1 = new String("tch");
String s2 = new String("tch");
System.out.println(s1 == s2);//F

# String 对象特性

  1. String 是一个 final 类,代表不可变的字符序列
  2. 字符串是不可变的,一个字符串对象一旦被分配,其内容是不可改变的
  3. 常量相加,看的是常量池;变量相加,看的是堆
1
2
3
4
5
6
7
8
9
//一共有三个对象
String a = "hello";
String b = "abc";
String c = a + b;
//1.先创建一个StringBuilder sb = StringBuilder()
//2.执行sb.append("hello");
//3.执行sb.append("abc");
//4.String c = sb.toString();
//最后c指向堆中的对象(String)value[]-->池中的"helloabc"

# String 类的常见方法

String 类是保存字符串常量的,每次更新都需要重新开辟空间,效率较低,因此 java 设计者还提供了 StringBuilder 和 StringBuffer 来增强 String 的功能。

1
2
3
4
substring(1,4);//左闭右开,截取从1到4减1的字符串
indexOf();//获取字符在字符串对象中第一次出现的索引,从0开始,若找不到返回-1
toUpperCase(); toLowerCase(); concat(); replace();
split(); compareTo(); toCharArray(); format();

# StringBuffer 类

  1. java.lang.StringBuffer 代表可变的字符序列,可以对字符串内容进行增删
  2. 很多方法与 StringBuffer 相同,但 StringBuffer 是可变长度的
  3. StringBuffer 是一个容器
1
2
3
4
5
6
StringBuffer str1 = new StringBuffer("hello");
//1.StringBuffer的直接父类是AbstractStringBuilder
//2.StringBuffer实现了序列化
//3.在父类中AbstractStringBuilder有属性char[] value,且不是final
//4.该value数组存放字符串内容,因此他是存放在堆中的
//5.StringBuffer是一个final类,不能被继承

# String 和 StringBuffer 的对比

  1. String 保存的是字符串常量,里面的值不能更改,每次 String 类的更新,实际上是更改地址,效率比较低
  2. StringBuffer 保存的是字符串变量,里面的值可以修改,每次 StringBuffer 的更新实际上可以更新内容,不用每次更改地址,效率较高(空间大小不够的时候会扩容,也就会更改地址)

# StingBuffer 的构造器

1
2
3
4
5
6
7
8
9
StringBuffer()	//创建一个大小为16的char[],用于存放字符内容

StringBuffer(CharSequence seq) //构造一个字符串缓冲区,它包含与指定的CharSequence相同的字符

StringBuffer(int capacity) //指定char[]的大小

StringBuffer(String str) //通过给一个String来创建StringBuffer

StringBuffer hello = new StringBuffer("hello");

# String 和 StringBuffer 相互转换

# String —> StringBuffer

1
2
3
4
5
6
String s = "helllo";
//way 1
StringBuffer b1 = new StringBuffer(s);
//way 2
StringBuffer b2 = new StringBuffer();
b2.appends(s);

# StringBuffer —> String

1
2
3
4
//way 1
String s2 = b1.toString(); //b1是[StringBuffer]
//way 2
String s3 = new String(b1);

# StringBuffer 类常见方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
StringBuffer s = new StringBuffer("hello");
//增
s.append(',');
s.append("hi");
s.append("123").append(100).append(true).append(1.5);
//删:左闭右开
delete(start, end)
//改:将始终位置的内容替换为string,左闭右开
replace(start, end, string)
//查:查找子串在字符串第一次出现的索引,找不到返回-1
indexOf()
//插:在指定位置插入字符串
insert(pos, string)
//获取长度:
length

# StringBuilder 类

  1. 一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快
  2. 在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据
  3. StringBuilder 的常用方法和 StringBuffer 是一样的
  4. StringBuilder 是 final 类,不能被继承
  5. StringBuilder 对象字符序列也是存放在父类 AbstractStringBuilder 的 char [] value 中,所以也是存放在堆中
  6. 多线程使用有风险,源码中没有线程同步的操作,也就是没有 synchronized 关键字

# String,StringBuffer 和 String Builder 的对比

  1. String、StringBuffer 和 StringBuilder 都代表可变的字符序列,且方法一样

  2. String:不可变字符序列,效率低,但是复用率高

  3. StringBuffer:可变字符序列,效率较高(增删),线程安全

  4. StringBuilder:可变字符序列,效率最高,但线程不安全

  5. String 使用注意说明

    1
    2
    string s = "a";
    s += "b";

    这串代码中,原本的字符串对象 "a" 是被丢弃了之后在产生的 "ab",多次执行这种改变内容的操作会导致大量副本对象存留在内存中,降低效率,尤其是当这种操作在循环中时。所以在需要对 String 对象做大量的修改时,不要用 String

# Math 类

  1. abs 绝对值

    1
    int abs = Math.abs(-1);//1
  2. pow 求幂

    1
    double pow = Math.pow(2, 4);//2的四次方
  3. ceil 向上取整

    1
    double ceil = Math.ceil(-3.001);//-3.0
  4. floor 向下取整

    1
    double floor = Math.floor(-4.99);//-5.0
  5. round 四舍五入

    1
    long round = Math.round(-5.001);//-5
  6. sqrt 开方

    1
    double sqrt = Math.sqrt(9.0)//3.0
  7. random 求随机数

    1
    2
    3
    double random = Math.random();//返回一个[0,1)之间的随机小鼠
    //如果要让它取到一个设定范围内的整数,比如[2, 7],可以这么写
    int random = (int)(2 + Math.random() * (7 - 2 + 1));//因为需要取整,所以右边需要加1
  8. max、min 求最值

    1
    2
    int min = Math.min(1, 10);
    int max = Math.max(1, 10);

# Arrays 类

Arrays 类用于管理或操作数组(比如排序、搜索)

  1. toString:返回数组的字符串形式

    1
    Arrays.toString(arr);
  2. sort 排序:自然排序和定制排序(忘记了可看 p483)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //自然排序
    Integer arr[] = {1, -1, 4, 5, 0};
    Arrays.sort(arr);
    //定制排序,传入一个接口Comparator实现定制排序
    Arrays.sort(arr, new Comparator() {
    @override
    public int compare(Object o1, Object o2) {
    Integer i1 = (Integer o1);
    Integer i2 = (Integer o2);
    return i1 - i2;
    }
    });
    //实现了Comparator接口的匿名内部类,要求实现compare方法
  3. binarySearch:通过二分法进行查找,只能对有序数组使用

    1
    int index = Arrays.binarySearch(arr, 3);
  4. copyOf 数组元素复制

    1
    Integer[] newArr = Arrays.copyOf(arr, arr.length);
  5. fill:数组元素的填充

    1
    2
    Integer[] num = new Integer[] {5, 3, 1};
    Arrays.fill(num, 99);//将数组用99填充
  6. equals:比较两个数组的元素内容是否完全一致

    1
    boolean equals  = Arrays.equals(arr1, arr2);
  7. asList:将一组值转换成 list

    1
    2
    3
    4
    List<Integer> asList = Arrays.asList(2,3,4,5,6,1);//asList会将这些数据转为一个List集合
    System.out.println("asList=" + asList);
    //返回asList编译类型List(接口)
    //asList的运行类型是java.util.Arrays$ArrayList

# System 类

  1. exit:退出当前程序

  2. arraycopy:复制数组元素,比较适合底层调用,一般情况还是用 Arrays.copyOf。

    1
    2
    3
    4
    int[] src = {1,2,3};
    int[] dest = new int[3];
    System.arraycopy(src, 0, dest, 0, 3);
    //把src从第0个位置开始的元素,复制给dest,从0位置开始,复制3个元素
  3. currentTimeMillens:返回当前时间距离 1970-1-1 的毫秒数

  4. gc:运行垃圾回收机制

    1
    System.gc();

# BigInteger 和 BigDecimal 类

  1. BigInteger 适合保存比较大的整型
  2. BigDecimal 适合保存精度更高的浮点型?
1
2
3
4
5
BigInteger a = new BigInteger("9999999999999999999999");
BigInteger b = new BigInteger("100");

BigDecimal x = new BigDecimal("15.156448946513165453");
BigDecimal y = new BigDecimal("45.166431135")

加减乘除不能直接用传统的方法,要用下面的方法来进行

# 常见方法

  1. add

    1
    2
    BigInteger add = bigInteger.add(b);
    BigDecimal add = bigdecimal.add(b);
  2. subtract:减

    1
    2
    BigInteger subtract = bigInteger.subtract(b);
    BigDecimal subtract = bigdecimal.subtract(b);
  3. multiply

    1
    2
    BigInteger multiply = bigInteger.multiply(b);
    BigDecimal multiply = bigdecimal.multiply(b);
  4. divide:除

    1
    2
    3
    4
    5
    BigInteger divide = bigInteger.divide(b);
    BigDecimal divide = bigdecimal.divide(b);

    //在除法中可能会出现无限循环小数,这里可以指定精度:BigDecimal.ROUND_CELING
    System.out.println("bigDecimal.divide(b, a.ROUND_CELING)")

# 日期类

  1. Date:精确到毫秒
  2. SimpleDateFormat:格式和解析日期的类。它允许进行格式化(日期 -> 文本),解析(文本 -> 日期)和规范化
1
2
3
4
5
6
7
8
9
10
11
12
13
Date d1 = new Date();
//默认输出格式一般是国外的惯用格式,这里可以格式化
SimpleDateFormat day = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
String format = day.format(d1);
System.out.println("当前日期为:" + format);

//可以通过毫秒数来指定此毫秒数对应的时间
Date d2 = new Date(923898);
System.out.println("d2=" + d2);

//还可以把一个格式化的String转换成对应的Date。(格式一定要正确)
String s = "1996年01月01日 10:20:30 星期一";
Date parse = day.parse(s);

# 第二代日期类

  1. Calendar 类

    1
    public abstract class Calendar extends Object implements Serializable, Cloneable, Comparable<Calendar>
  2. Calendar 类是一个抽象类,为 YEAR, MONTH, DAY_OF_MONTH, HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法

1
2
3
4
5
6
7
8
9
10
//Calendar是一个抽象类,且构造器是私有的,所以可以通过getInstance()来获取实例
//这个就比较灵活,可以需要什么就get什么,随意组合
Calendar c = Calendar.getInstance();
System.out.println(c.get(Calendar.DAY_OF_WEEK));
System.out.println(c.get(Calendar.YEAR));
System.out.println(c.get(Calendar.MONTH) + 1);//月要加1,因为Calendar返回月的时候是从0开始编号的
System.out.println(c.get(Calendar.DAY_OF_MONTH));
System.out.println(c.get(Calendar.HOUR));
System.out.println(c.get(Calendar.MINUTE));
System.out.println(c.get(Calendar.SECOND));

# 第三代日期类

JDK 1.0 中包含了一个 java.util.Date 类,但它的大多数方法已经在 JDK 1.1 引入 Calendar 类后被弃用了,而 Calendar 类也存在一些问题

  1. 可变性:日期和时间这样的类应该是不可变的
  2. 偏移性:Date 中的年份是从 1900 年开始的,而月份都从 0 开始
  3. 格式化:格式化只对 Date 有用,Calendar 不可以
  4. 线程不安全,不能处理闰秒(每隔 2 天,多出 1s)

# 第三代日期类常见方法

LocalDate (日期 / 年月日)、LocalTime (时间 / 时分秒)、LocalDateTime (日期时间) JDK8 加入

1
2
3
4
5
6
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
System.out.println(ldt.getYear());
System.out.println(ldt.getMonth());
System.out.println(ldt.getDayOfMonth());
System.out.println(ldt.getHour());

# 格式日期类 DateTimeFormatter

1
2
3
//先创建一个DateTimeFormatter对象
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH小时mm分钟ss秒");
String format = dtf.format(ldt)

# 时间戳

类似于 Date,提供了一系列和 Date 类转换的方式

1
2
3
4
5
6
Instant now = Instant.now();
//通过方法转换:
//Instant --> Date
Date date = Date.from(instant);
//Date --> Instant
Instant instant = date.toInstant();

# 更多方法

  1. LocalDateTime 类
  2. MonthDay 类:检查重复事件
  3. 是否是闰年
  4. 增加日期的某个部分
  5. 使用 plus 方法增加时间的某个部分
  6. 使用 minus 方法查看一年前和一年后的日期

# 集合(Collection)

集合主要是两组,即单列集合和双列集合。

  1. Collection 接口有两个重要的子接口,List 和 Set,他们实现子类都是单列集合
  2. Map 接口的实现子类是双列集合,存放 K-V
1
2
3
4
5
6
7
ArrayList al = new ArrayList();
al.add("jack");
al.add("tom");

HashMap hm = new HashMap();
hm.put("No.1", "tch");
hm.put("No.2", "hct");

# 集合和数组的对比

数组:

  1. 长度开始时必须指定,且指定后就不能修改
  2. 保存的必须为同一类型的元素
  3. 使用数组进行增加元素的示意代码比较麻烦

而集合:

  1. 可以动态保存任意多个对象,使用比较方便
  2. 提供了一系列方便的操作对象的方法:add、remove、set、get 等
  3. 使用集合添加,删除新元素的示意代码,更加简洁

# Collection 接口和常用方法

# Collection 接口实现类的特点

public interface Collection<E> extends Iterable<E>

  1. Collection 实现子类可以存放多个元素,每个元素可以是 Object
  2. 有些 Collection 的实现类,可以存放重复的元素,有些不可以
  3. Collection 的实现类,有些是有序的(List),有些是无序的(Set)
  4. Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的

# Collection 接口常用方法

以实现子类 ArrayList 来演示

1
List list = new ArrayList();
  1. add:添加单个元素

    1
    2
    3
    list.add("tch");
    list.add(10);
    list.add(true);
  2. remove:删除指定元素

    1
    2
    list.remove(0);//删除索引位置为0的元素
    list.remove("tch");//指定删除
  3. contains:查找元素是否存在

    1
    list.contains("tch");
  4. size:获取元素个数

    1
    list.size();
  5. isEmpty:判断是否为空

    1
    list.isEmpty();
  6. clear:清空

    1
    list.clear();
  7. addAll:添加多个元素

    1
    2
    3
    ArrayList list2 = new ArrayList();
    list2.add("tch2");
    list.addAll(list2);
  8. containsAll:查找多个元素是否都存在

    1
    list.containsAll(list2);
  9. removeAll:删除多个元素

    1
    list.removeAll(list2);

# Collection 接口遍历元素方式

# 使用 Iterator(迭代器)

  1. Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素
  2. 所有实现了 Collection 接口的集合类都有一个 iterator () 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器
  3. Iterator 仅用于遍历集合,Iterator 本身并不存放对象

工作原理:

1
2
3
4
5
Iterator iterator = coll.iterator();//得到一个集合的迭代器
//hasNext()判断是否还有下一个元素
while(iterator.hasNext()){//next()的作用:指针下移、将下移以后的位置上的元素返回
System.out.println(iterator.next());
}//快捷键itit

在调用 iterator.next () 方法之前必须要调用 iterator.hasNext () 进行检测,若不调用,且下一条记录无效时,直接调用 iterator.next () 会抛出 NoSuchElementException 异常

如果要再次使用,需要重置迭代器 iterator = coll.iterator();

# 使用 for 循环增强

增强 for 就是简化版的 iterator,本质一样,只能用于遍历集合或数组。底层其实就是迭代器

1
2
for(Object object : col){
}//快捷键I

# List 接口和常用方法

List 接口是 Collection 接口的子接口

  1. List 集合类中元素有序(即添加顺序和取出顺序一致)且可重复
  2. List 集合中的每个元素都有对应的顺序索引(索引从 0 开始)
  3. List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  4. 常用的 List 接口的实现类:ArrayList、LinkedList、Vector
1
2
3
4
5
6
7
8
void add(int index, Object ele)//在index位置插入ele元素
boolean addAll(int index, Collection eles)//从index位置开始将eles中的所有元素添加进来
Object get(int index)//获取指定index位置的元素
int indexOf(Object obj)//返回obj在集合中首次出现的位置
int lastlndexOf(Object obj)//返回obj在当前集合中末次出现的位置
Object remove(int index)//移除指定 indiex位言的完素,井返回此元
Object set(int index, Object ele)//设置指定index位置的元素为ele,相当于是替换.
List subList(int fromlndex, int tolndex)//返回从fromlndex到tolndex位置的子集合

List 和 ArrayList 的区别:Java 中 List 和 ArrayList 的区别及使用_java list arraylist-CSDN 博客

# ArrayList 的底层结构和源码分析

# 注意事项

  1. permits all elements (可以放所有元素), including null。ArrayList 可以加入 null,并且可以加入多个
  2. ArrayList 是由数组来实现数据存储的
  3. ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全(但执行效率高),但在多线程情况下不建议使用 ArrayList

# 源码分析

  1. ArrayList 中维护了一个 Object 类型的数组 elementData. [debug 看源码]transient Objectil elementData;
  2. 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0,第 1 次添加,则扩容 elementData 为 10,如需要再次扩容,则扩容 elementData 为 1.5 倍
  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍

# Vector 的底层结构和源码分析

# 注意事项

  1. Vector 类的定义说明

    1
    2
    public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  2. Vector 底层也是一个对象数组, protected Object[] elementData;

  3. Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 synchronized

  4. 在开发中需要线程同步安全的,考虑使用 Vector

# 源码分析

  1. 创建对象时,如果使用的无参构造器,默认容量为 10,满了之后按照 2 倍扩容
  2. 创建对象时,如果是指定大小,则每次直接按照 2 倍扩容

# LinkList 的底层结构

  1. LinkList 实现了双向链表和双端队列的特点
  2. 可以添加任意元素(可重复),包括 null
  3. 线程不安全,没有实现同步

# 底层操作机制

  1. LinkList 底层是一个双向链表
  2. LinkList 中维护了两个属性 first 和 last 分别指向首结点和尾结点
  3. 每个结点 (Node 对象) 里面有维护了 prev、next、item 三个属性,其中 prev 指向前一个,next 指向后一个,实现双向链表
  4. 所以 LinkList 的元素的添加和删除,不是通过数组完成的,效率较高

IMG_0027

# ArrayList 和 LinkedList 的比较

如何选择:

  1. 改查多,选 ArrayList
  2. 增删多,选 LinkedList
  3. 一般来说在程序中,80%-90%都是查询,因此大部分情况下会选择 ArrayList
  4. 在一个项目中根据业务灵活选择,也有可能一个模块使用的是 ArrayList,另一个模块是 LinkedList

# Set 接口和常用方法

  1. 无序,没有索引
  2. 不允许重复元素,所以最多包含一个 null
  3. JDK API 中 set 接口的实现类有 HashSet、TreeSet 等等
  4. set 接口对象不能通过索引来获取,且没有 get 方法,所以不能用普通 for 循环遍历

# HashSet

  1. HashSet 实现了 Set 接口,可存放 null 但只能有一个
  2. HashSet 实际上是 HashMap
  3. HashSet 不抱着元素是有序的,取决于 hash 后,在确定索引的结果
  4. 不能有重复对象

# 方法注意事项

  1. add 方法在执行后会返回一个 boolean 值,如果添加成功返回 true,反之返回 false
  2. 可以通过 remove 指定删除对象

# HashSet 底层机制

HashSet 底层是 HashMap,HashMap 底层是数组 + 链表 + 红黑树

  1. 添加一个元素时,先得到 hash 值,再转成索引值
  2. 找到储存数据表 table,看这个索引位置是否已经存放的有元素
  3. 如果没有直接加入
  4. 如果有,就调用 equals 比较,如果相同就放弃添加,反之添加到最后
  5. 在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8),且 table 的大小 >=MIN_TREEIFY_CAPACITY (默认 64),就会进行树化(红黑树)

扩容机制:

  1. 第一次添加时,table 扩容到 16,临界值 (threshold) 是 16 * 加载因子 (loadFactor),加载因子 = 0.75,结果就是 12
  2. 如果 table 数组使用到了临界值 12,就会扩容到 16*2=32 ,新的临界值就是 32*0.75 ,以此类推
  3. 在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8),并且 table 的大小 >=MIN_TREEIFY_CAPACITY (默认 64),就会进行树化(红黑树),否则仍然采用数组扩容机制
  4. 要注意的是,在一个 table 中只要添加了 0.75 倍的 Node,就会进行扩容,无论是添加到某个结点,还是某个结点的链表上

# LinkedHashSet

  1. LinkedHashSet 是 HashSet 的子类
  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组 + 双向链表
  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的储存位置,同时使用链表维护元素的次序 (图),这使得元素看起来是以插入顺序保存的
  4. LinkedHashSet 不允许添加重复元素

# TreeSet

//////

# 图(Map)

1
2
3
4
5
Map map = new HashMap();
map.put("no1", "tch1");
map.put("no2", "tch2");
map.put("no1", "tch3");
map.put("no3", "tch1");

Map 接口实现类的特点:

  1. Map 与 Collection 并列存在,用于保存具有映射关系的数据:Key-Value
  2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
  3. Map 中的 key 不允许重复,原因和 HashSet 一样(当 key 相同时,新的那个 key 的 value 会替换旧的 key 的 value)
  4. Map 中的 value 可以重复
  5. Map 的 key 可以为 null,value 也可以为 null,但 key 为 null 只能有一个,而 value 可以有多个
  6. 常用 String 类作为 Map 的 key
  7. key 和 value 之间存在单向一对一关系,即通过指定的 key 能找到对应的 value(用 get 方法,通过 key 值查找)
  8. 一对 k-v 是放在一个 Node 中的,因为 Node 实现了 Entry 接口(有些书上说,一对 k-v 就是一个 Entry)

# 对 Map.Entry 的解释

Map.Entry 是 Map 中的一个接口,他的用途是表示一个映射项(里面有 Key 和 Value),而 Set<Map.Entry<K,V>> 表示一个映射项的 Set。Map.Entry 里有相应的 getKey 和 getValue 方法,即 JavaBean,让我们能够从一个项中取出 Key 和 Value。

因为 Map 这个类没有继承 Iterable 接口,所以不能直接通过 map.iterator 来遍历,所以就只能先转化为 set 类型,用 entrySet () 方法,其中 set 中的每一个元素值就是 map 中的一个键值对,也就是 Map.Entry<K,V> 了,然后就可以遍历了。


Map 是 java 中的接口,Map.Entry 是 Map 的一个内部接口。

Map 提供了一些常用方法,如 keySet ()、entrySet () 等方法,keySet () 方法返回值是 Map 中 key 值的集合;entrySet () 的返回值也是返回一个 Set 集合,此集合的类型为 Map.Entry。

Map.Entry 是 Map 声明的一个内部接口,此接口为泛型,定义为 Entry<K,V>。它表示 Map 中的一个实体(一个 key-value 对)。接口中有 getKey (),getValue 方法

Node<K,V> 实现了 Entry 接口,Entry 接口中 K 表示 key, 即键,V 表示 value, 即值。Entry 即 Node 是 Map 集合中的一个对象元素,而 Map 集合正是由一个个 Entry 即 Node 对象所构成。
正是因为 Node 实现了 Entry 接口,所以使用 Entry 的时候也可以使用其 getValue () 和 getKey () 方法

1Java 中 Map.Entry 详解_java map.entry-CSDN 博客

# Map 接口的常用方法

  1. put:添加
  2. remove:根据键删除
  3. get:根据键获取
  4. size:元素个数
  5. isEmpty:判断是否为空
  6. clear:清除
  7. containsKey:查找键是否存在

# Map 接口遍历方法

  1. containsKey:查找键是否存在

  2. keySet:获取所有键

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //第一组:先取出所有key,再通过key取出对应的value
    Set set = map.keySet();
    //(1)增强for
    for (Object key : set) {
    System.out.println(key + ":" + map.get(key));
    }
    //(2)迭代器
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
    Object next = iterator.next();
    System.out.println(next + ":" + map.get(next));
    }
  3. values:获取所有值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //第二组:取出所有的value
    Collection values = map.values();
    //(1)增强for
    for (Object value : values) {
    System.out.println(value);
    }
    //(2)迭代器
    Iterator iterator2 = values.iterator();
    while (iterator2.hasNext()) {
    Object value = iterator2.next();
    System.out.println(value);
    }
  4. entrySet:获取所有关系 k-v

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //第三组:通过EntrySet获取k-v
    Set entrySet = map.entrySet();//EntrySet<Map.Entry<K,V>>
    //(1)增强for
    for (Object entry : entrySet) {
    //将entry对象转成Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + ":" + m.getValue());
    }
    //(2)迭代器
    Iterator iterator3 = entrySet.iterator();
    while (iterator3.hasNext()) {
    Object entry = iterator3.next();
    //向下转型Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + ":" + m.getValue());
    }

# Hashtable

  1. 存放元素是键值对:k-v
  2. hashtable 的键和值哦都不能为 null
  3. hashTable 使用方法基本和 HashMap 一样
  4. hashTable 线程安全,hashMap 线程不安全

# 底层

  1. 底层数组 Hashtable$Entry [] 初始化大小为 11
  2. 临界值 threshold 8 = 11 * 0.75
  3. 用方法 addEntry (hash, key, value, index) 添加 k-v,封装到 entry
  4. 当 if (count>= threshold) 满足时进行扩容

# Properties

  1. properties 类继承自 Hashtable 类,且实现了 Map 接口,也是使用键值对来储存数据
  2. 使用特点和 Hashtable 类似
  3. properties 可以用于从 xxx.properties 文件中加载数据到 Properties 类对象,并进行读取和修改

# 开发时如何选择