java学习

面向对象-基础

类和对象的内存分配机制

Java 内存的结构分析

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

举一个创建对象的流程分析例子

1
2
3
Person p = new Person();
p.name = “jack”;
p.age = 10
  • 先加载 Person 类信息(属性和方法信息, 只会加载一次)
  • 在堆中分配空间, 进行默认初始化(看规则)
  • 把地址赋给 p , p 就指向对象
  • 进行指定初始化, 比如 p.name =”jack” p.age = 10

成员方法调用机制示意图

image-20240729165725998

递归

例题一

image-20240729211658022

下面为该题代码

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
public class Test {
public static void main(String[] args) {
T t1 = new T();
int n = 7;
int res = t1.fibonacci(n);
if(res != -1) {
System.out.println("当 n="+ n +" 对应的斐波那契数=" + res);
}
int day = 1;
int peachNum = t1.peach(day);
if(peachNum != -1) {
System.out.println("第 " + day + "天有" + peachNum + "个桃子");
}
}
}

class T {
public int fibonacci(int n) {
if(n == 1 || n == 2) {
return 1;
}else{
return fibonacci(n-1) + fibonacci(n-2);
}
}
public int peach(int day){
if(day == 10) {//第 10 天,只有 1 个桃
return 1;
} else if ( day >= 1 && day <=9 ) {
return (peach(day + 1) + 1) * 2;//规则,自己要想
} else {
System.out.println("day 在 1-10");
return -1;
}
}
}

汉诺塔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StringToBasicDetail {
public static void main(String[] args) {
Tower tower = new Tower();
tower.move(5, 'A', 'B', 'C');
}
}

class Tower{
public void move(int num, char a, char b, char c){
//如果只有一个盘 num = 1
if(num == 1) {
System.out.println(a + "->" + c);
} else {
//如果有多个盘,可以看成两个 , 最下面的和上面的所有盘(num-1)
//(1)先移动上面所有的盘到 b, 借助 c
move(num - 1 , a, c, b);
//(2)把最下面的这个盘,移动到 c
System.out.println(a + "->" + c);
//(3)再把 b 塔的所有盘,移动到 c ,借助 a
move(num - 1, b, a, c);
}
}
}

可变参数

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

基本语法如下所示:

1
2
访问修饰符 返回类型 方法名(数据类型... 形参名) {
}

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringToBasicDetail {
public static void main(String[] args) {
HspMethod m = new HspMethod();
System.out.println(m.sum(1, 5, 100));
System.out.println(m.sum(1,19));
}
}

class HspMethod{
public int sum(int... nums){
//1. int... 表示接受的是可变参数,类型是 int ,即可以接收多个 int(0-多)
//2. 使用可变参数时,可以当做数组来使用 即 nums 可以当做数组
//3. 遍历 nums 求和即可
int res = 0;
for(int i = 0; i<nums.length; i++){
res += nums[i];
}
return res;
}
}

image-20240801102028258

可变参数与普通参数一起的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringToBasicDetail {
public static void main(String[] args) {
HspMethod m = new HspMethod();
System.out.println(m.showScore("Sherlock",90.1,80,87.8 ));
}
}

class HspMethod{
public String showScore(String name, double... scores){
double total = 0;
for(int i = 0; i<scores.length; i++){
total += scores[i];
}
return name + ":" + total;
}
}

构造器

基础

构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。

它有几个特点:

  • 方法名和类名相同
  • 没有返回值
  • 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化
1
2
3
4
5
6
7
8
9
10
11
12
class Person {
String name;
int age;
//1. 构造器没有返回值, 也不能写 void
//2. 构造器的名称和类 Person 一样
//3. (String pName, int pAge) 是构造器形参列表,规则和成员方法一样
public Person(String pName, int pAge) {
System.out.println("构造器被调用~~ 完成对象的属性初始化");
name = pName;
age = pAge;
}
}

一个类也可以有多个构造器,即构造器重载

创建对象的时候,会自动调用该类的构造方法

this关键字

this表示的就是当前对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StringToBasicDetail {
public static void main(String[] args) {
Dog dog1 = new Dog("小黑",3);
dog1.info();
}
}

class Dog{
String name;
int age;
Dog(String name,int age){
this.name = name;
this.age = age;
}
void info(){
System.out.println("this.hashCode=" + this.hashCode());
System.out.println(name + "\t" + age + "\t");
}
}

面向对象-中级

包(package)

包(package)是用于组织类和接口的命名空间。包提供了一种将相关的类和接口进行分组的机制,方便代码的管理和维护。通过使用包,可以避免类名冲突,增强代码的可读性和可维护性

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

包的声明用package关键字,引入用import关键字,如下:
image-20240801165801215

我们需要使用到哪个类,就导入哪个类即可,不建议使用 *导入

1
2
import java.util.Scanner; //表示只会引入 java.util 包下的 Scanner
import java.util.*;//表示将 java.util 包下的所有类都引入(导入)

访问修饰符

java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):

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

image-20240801155402755

继承

  1. 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访 问,要通过父类提供公共的方法去访问
  2. 子类必须调用父类的构造器, 完成父类的初始化
  3. 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过
  4. 如果希望指定去调用父类的某个构造器,则显式的调用一下 : super(参数列表)
  5. super 在使用时,必须放在构造器第一行(super只能在构造器中使用)
  6. super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
  7. java 所有类都是 Object 类的子类, Object 是所有类的基类.
  8. 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类)
  9. 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制
  10. 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系

这里来解释一下super()和this()之间的区别所在

super()

  1. 调用父类构造方法super()用于在子类的构造方法中调用父类的构造方法。这样做是为了确保父类的构造方法在子类的构造方法之前执行。
  2. 必须是构造方法的第一行super()调用必须是子类构造方法中的第一条语句。如果没有显式地调用super(),编译器会在没有参数的情况下隐式地调用父类的无参构造方法。
  3. 参数传递:可以使用参数将值传递给父类的构造方法,例如super(param1, param2)

来个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
Parent() {
System.out.println("Parent Constructor");
}
}

class Child extends Parent {
Child() {
super();
System.out.println("Child Constructor");
}
}

public class Test {
public static void main(String[] args) {
Child child = new Child();
}
}

this()

  1. 调用当前类的构造方法this()用于调用当前类的另一个构造方法。这通常用于构造方法之间的重用代码。
  2. 必须是构造方法的第一行:与super()类似,this()调用也必须是构造方法中的第一条语句。
  3. 参数传递:可以使用参数将值传递给当前类的另一个构造方法,例如this(param1, param2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Example {
Example() {
this("Hello");
System.out.println("Default Constructor");
}

Example(String msg) {
System.out.println("Parameterized Constructor: " + msg);
}
}

public class Test {
public static void main(String[] args) {
Example example = new Example();
}
}

super关键字

super 代表父类的引用,用于访问父类的属性、方法、构造

image-20240802162455190

super关键字不会访问当前类(本类)的成员和方法

访问父类的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
int x = 10;
}

class Child extends Parent {
int x = 20;

void display() {
System.out.println("Child x: " + x);
System.out.println("Parent x: " + super.x); // 访问父类的成员变量
}
}

public class Main {
public static void main(String[] args) {
Child child = new Child();
child.display();
}
}

访问父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
void display() {
System.out.println("Display from Parent");
}
}

class Child extends Parent {
void display() {
System.out.println("Display from Child");
super.display(); // 调用父类的方法
}
}

public class Main {
public static void main(String[] args) {
Child child = new Child();
child.display();
}
}

super关键字所带来的便利

image-20240802164455339

方法覆写

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

以下是几个需要注意的点:

方法签名

  • 覆写的方法必须具有与父类方法相同的方法签名,包括方法名和参数列表。

访问修饰符

  • 子类方法的访问修饰符不能比父类方法的更严格。例如,如果父类方法是public,子类方法也必须是public

返回类型

  • 覆写方法的返回类型可以是父类方法返回类型的子类型(协变返回类型),但不能是其他不相关的类型。(父类 返回类型是 Object ,子类方法返回类型是 String)

异常处理

  • 子类方法抛出的异常不能比父类方法抛出的异常更广泛。例如,如果父类方法抛出一个IOException,子类方法只能抛出IOException或其子类的异常,不能抛出Exception或其他更泛的异常。

使用@Override注解

  • 在覆写的方法上加上@Override注解有助于编译器检查是否正确地覆写了父类的方法。如果没有正确覆写,编译器会报错。

调用父类方法

  • 在子类方法中,可以使用super关键字调用父类的方法。例如:super.methodName()

静态方法不能覆写

  • 静态方法不能被覆写(Override),而是被隐藏(Hide)。如果子类定义了一个与父类静态方法相同的方法,这只是隐藏了父类的方法,并不是覆写。

final方法不能覆写

  • 如果父类的方法被声明为final,那么子类不能覆写这个方法。

构造方法不能覆写

  • 构造方法不能被继承或覆写。
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
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}

public void eat() throws IOException {
System.out.println("Animal eats");
}
}

class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}

@Override
public void eat() throws IOException {
System.out.println("Dog eats");
}
}

public class Main {
public static void main(String[] args) throws IOException {
Animal myDog = new Dog();
myDog.makeSound(); // 输出:Dog barks
myDog.eat(); // 输出:Dog eats
}
}

重写与覆写之间的区别

image-20240803110711885

下面来一个实例:

  1. 编写一个 Person 类,包括属性/private(name、age),构造器、方法 say(返回自我介绍的字符串)
  2. 编写一个 Student 类,继承 Person 类,增加 id、score 属性/private,以及构造器,定义 say 方法(返回自我介绍的信息)
  3. 在 main 中,分别创建 Person 和 Student 对象,调用 say 方法输出自我介绍

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package show;

public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String say(){
return "name=" + name + " age=" + age;
}
}

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package show;

public class Student extends Person {
private int id;
private double score;
public Student(String name, int age, int id, double score) {
super(name, age);
this.id = id;
this.score = score;
}
public String say(){
return super.say() + " id=" + id + " score=" + score;//super()方法,代码复用
}
}

Test.java

1
2
3
4
5
6
7
8
9
10
11
import show.*;

public class Test {
public static void main(String[] args) {
//在 main 中,分别创建 Person 和 Student 对象,调用 say 方法输出自我介绍
Person jack = new Person("jack", 10);
System.out.println(jack.say());
Student smith = new Student("smith", 20, 123456, 99.8);
System.out.println(smith.say());
}
}

多态

方法的多态

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

对象的多态

image-20240805110807519

类Dog,Cat都是类Animal的子类,已经定义过了

1
2
3
4
5
6
7
8
9
10
11
12
public class PolyObject {
public static void main(String[] args) {
//体验对象多态特点
//animal 编译类型就是 Animal , 运行类型 Dog
Animal animal = new Dog();
//因为运行时 , 执行到改行时,animal 运行类型是 Dog,所以 cry 就是 Dog 的 cry
animal.cry(); //小狗汪汪叫
//animal 编译类型 Animal,运行类型就是 Cat
animal = new Cat();
animal.cry(); //小猫喵喵叫
}
}

因此,利用多态我们就可以完成下面这道编程题目

image-20240805111656544

对于这道题,当我们写完各种的子类之后,在上面类Master中的方法就只要写下面这个

image-20240805111811071

多态的注意事项和细节

多态的前提是:两个对象(类)存在继承关系

image-20240805115325906

举个例子,如下所示:

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
public class PolyDetail {
public static void main(String[] args) {
//向上转型: 父类的引用指向了子类的对象
//语法:父类类型引用名 = new 子类类型();
Animal animal = new Cat();
Object obj = new Cat();//可以吗? 可以 Object 也是 Cat 的父类
//向上转型调用方法的规则如下:
//(1)可以调用父类中的所有成员(需遵守访问权限)
//(2)但是不能调用子类的特有的成员
//(#)因为在编译阶段,能调用哪些成员,是由编译类型来决定的
//animal.catchMouse();错误
//(4)最终运行效果看子类(运行类型)的具体实现, 即调用方法时,按照从子类(运行类型)开始查找方法
//,然后调用,规则我前面我们讲的方法调用规则一致。
animal.eat();//猫吃鱼.. animal.run();//跑
animal.show();//hello,你好
animal.sleep();//睡
//希望可以调用 Cat 的 catchMouse 方法
//多态的向下转型
//(1)语法:子类类型 引用名 =(子类类型)父类引用;
//问一个问题? cat 的编译类型 Cat,运行类型是 Cat
Cat cat = (Cat) animal;
cat.catchMouse();//猫抓老鼠
//(2)要求父类的引用必须指向的是当前目标类型的对象
Dog dog = (Dog) animal; //可以吗?
System.out.println("ok~~");
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PolyDetail02 {
public static void main(String[] args) {
//属性没有重写之说!属性的值看编译类型
Base base = new Sub();//向上转型
System.out.println(base.count);// ? 看编译类型 10
Sub sub = new Sub();
System.out.println(sub.count);//? 20
}
}
class Base { //父类
int count = 10;//属性
}
class Sub extends Base {//子类
int count = 20;//属性
}

instanceOf 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的子类型

1
System.out.println(bb instanceof BB);

多态的应用

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

应用实例:现有一个继承结构如下:要求创建 1 个 Person 对象、2 个 Student 对象和 2 个 Teacher 对象, 统一放在数组 中,并调用每个对象 say 方法. 应用实例升级:如何调用子类特有的方法,比如 Teacher 有一个 teach , Student 有一个 study 怎么调用?

在已经写完Person,Student,Teacher类后的测试代码如下:

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
import show.*;

public class Test {
public static void main(String[] args) {
//应用实例:现有一个继承结构如下:要求创建 1 个 Person 对象、
// 2 个 Student 对象和 2 个 Teacher 对象, 统一放在数组中,并调用每个对象 say 方法
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18, 100);
persons[2] = new Student("smith", 19, 30.1);
persons[3] = new Teacher("scott", 30, 20000);
persons[4] = new Teacher("king", 50, 25000);
//循环遍历多态数组,调用 say
for (int i = 0; i < persons.length; i++) {
//person[i] 编译类型是 Person ,运行类型是是根据实际情况有 JVM 来判断
System.out.println(persons[i].say());//动态绑定机制
//这里使用 类型判断 + 向下转型.
if(persons[i] instanceof Student) {//判断 person[i] 的运行类型是不是Student
Student student = (Student)persons[i];//向下转型
student.study();
//也可以使用一句话 ((Student)persons[i]).study();
} else if(persons[i] instanceof Teacher) {
Teacher teacher = (Teacher) persons[i];
teacher.teach();
}
}
}

(2)多态参数

方法定义的形参类型为父类类型,实参类型可以为子类类型

实例就比如前面的主人喂食动物

面向对象-高级

类变量和类方法

类变量

在 Java 中,类变量(也称为静态变量static variable)是属于整个类而不是某个特定对象的变量。类变量在类加载时初始化,并且在类的所有实例之间共享。它们使用关键字 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
31
32
33
34
35
36
37
public class New {
public static void main(String[] args) {
//定义一个变量 count, 统计有多少小孩加入了游戏
int count = 0;
Child child1 = new Child("白骨精");
child1.join();
//count++;
child1.count++;
Child child2 = new Child("狐狸精");
child2.join();
//count++;
child2.count++;
Child child3 = new Child("老鼠精");
child3.join();
//count++;
child3.count++;
//===========
//类变量,可以通过类名来访问
System.out.println("共有" + Child.count + " 小孩加入了游戏...");
System.out.println("child1.count=" + child1.count);//3
System.out.println("child2.count=" + child2.count);//3
System.out.println("child3.count=" + child3.count);//3
}
}

class Child { //类
private String name;
//定义一个变量 count ,是一个类变量(静态变量) static 静态
//该变量最大的特点就是会被 Child 类的所有的对象实例共享
public static int count = 0;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + " 加入了游戏..");
}
}

注意

  • 类变量在类加载的时候就初始化了,也就是说即使你没有创建对象,只要类加载了,就可以使用类变量了
  • 当一个类的所有对象都共享一个变量的时候,考虑使用类变量:比如定义学生类,统计所有学生一共交了多少钱 Student(name,static fee)

类方法

类方法就是静态方法,形式为:访问修饰符 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class New {
public static void main(String[] args) {
//创建 2 个学生对象,交学费
Stu tom = new Stu("tom");
//tom.payFee(100);
Stu.payFee(100);//对不对?对
Stu mary = new Stu("mary");
//mary.payFee(200);
Stu.payFee(200);//对
//输出当前收到的总学费
Stu.showFee();//300
//如果我们希望不创建实例,也可以调用某个方法(即当做工具来使用)
//这时,把方法做成静态方法时非常合适
System.out.println("9 开平方的结果是=" + Math.sqrt(9));
System.out.println(MyTools.calSum(10, 30));
}
}
//开发自己的工具类时,可以将方法做成静态的,方便调用
class MyTools {
//求出两个数的和
public static double calSum(double n1, double n2) {
return n1 + n2;
}
}
//可以写出很多这样的工具方法... }
class Stu {
private String name;//普通成员
//定义一个静态变量,来累积学生的学费
private static double fee = 0;

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

//说明
//1. 当方法使用了 static 修饰后,该方法就是静态方法
//2. 静态方法就可以访问静态属性/变量
public static void payFee(double fee) {
Stu.fee += fee;//累积到
}

public static void showFee() {
System.out.println("总学费有:" + Stu.fee);

}
}

注意:静态方法只能访问静态成员,非静态的方法可以访问静态成员和非静态成员

代码块

代码化块又称为初始化块,属于类中的成员,类似于方法,但和方法不一样的是它没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用

语法:[修饰符]{代码};,要注意修饰符可选,要写的话也只能写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
class Movie {
private String name;
private double price;
private String director;
//3 个构造器-》重载
//(1) 下面的三个构造器都有相同的语句
//(2) 这样代码看起来比较冗余
//(3) 这时我们可以把相同的语句,放入到一个代码块中,即可
//(4) 这样当我们不管调用哪个构造器,创建对象,都会先调用代码块的内容
//(5) 代码块调用的顺序优先于构造器
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正是开始...");
};
public Movie(String name) {
System.out.println("Movie(String name) 被调用...");
this.name = name;
}
public Movie(String name, double price) {
this.name = name;
this.price = price;
}
public Movie(String name, double price, String director) {
System.out.println("Movie(String name, double price, String director) 被调用...");
this.name = name;
this.price = price;
this.director = director;
}
}

static代码块也叫做静态代码块,作用是对进行初始化,随类的加载而执行,并且只会执行一次,而普通的代码块没创建一个对象就执行

那么类什么时候会被加载呢

创建类的实例时

  • 当你使用 new 关键字创建一个类的对象时,JVM 会先检查该类是否已加载。如果没有加载,则会触发类的加载。

  • 示例:

    1
    MyClass obj = new MyClass(); // 创建对象,加载 MyClass 类

访问类的静态成员(静态变量或静态方法)时

  • 当你访问一个类的静态变量或调用静态方法时,如果类尚未加载,JVM 会加载该类。

  • 示例:

    1
    2
    System.out.println(MyClass.staticVariable); // 访问静态变量,加载 MyClass 类
    MyClass.staticMethod(); // 调用静态方法,加载 MyClass 类

类的初始化(静态代码块)

  • 类在加载时,静态代码块会被执行。这通常发生在静态成员被访问或类被实例化之前。

  • 示例:

    1
    2
    3
    4
    5
    6
    class MyClass {
    static {
    System.out.println("MyClass loaded");
    }
    }
    // 任何访问 MyClass 的操作都会触发静态代码块的执行

子类初始化时

  • 当一个子类被初始化时,如果其父类尚未被加载,那么父类会先被加载。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Parent {
    static {
    System.out.println("Parent loaded");
    }
    }

    class Child extends Parent {
    static {
    System.out.println("Child loaded");
    }
    }

    public class Main {
    public static void main(String[] args) {
    new Child(); // 加载顺序:先加载 Parent 类,再加载 Child 类
    }
    }

final关键字

final 关键字的用法

  1. 用于类(final class

    • 含义:如果一个类被声明为 final,则不能被继承。这意味着没有其他类可以扩展这个类。

    • 用途:使用 final 类通常是为了确保该类的实现不会被更改或扩展。例如,java.lang.String 类就是一个 final 类,它不能被继承。

    • 示例

      1
      2
      3
      public final class Constants {
      // 该类不能被继承
      }
  2. 用于方法(final method

    • 含义:如果一个方法被声明为 final,则子类不能重写这个方法。

    • 用途:使用 final 方法通常是为了防止子类更改方法的核心行为,确保方法的功能保持不变。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Parent {
      public final void display() {
      System.out.println("This is a final method.");
      }
      }

      class Child extends Parent {
      // 下面的代码会导致编译错误,因为 final 方法不能被重写
      // public void display() {
      // System.out.println("Attempt to override.");
      // }
      }
  3. 用于变量(final variable

    • 含义:如果一个变量被声明为 final,则它的值一旦被初始化,就不能再更改。

    • 用途

      • 对于 基本数据类型(如 intchar 等),final 变量的值在初始化后不可变。
      • 对于 引用数据类型(如对象、数组),final 变量的引用在初始化后不可更改(即不能指向另一个对象),但对象的内容可以改变。
    • 示例

      1
      2
      3
      4
      5
      final int MAX_VALUE = 100; // 基本数据类型,值不能更改

      final Person person = new Person("John");
      // person = new Person("Doe"); // 错误:不能改变引用
      person.setName("Doe"); // 合法:对象内容可以改变

特别说明

  1. final 和类加载的优化

    • JVM 和编译器可以利用 final 关键字进行优化。例如,一个 final 变量的值是已知的,可以在编译时内联(inlining),从而提高性能。
  2. final 和不可变对象

    • 使用 final 关键字是创建不可变对象的重要步骤之一。不可变对象的所有字段都应该是 final,以确保其状态在构造后不会改变。
  3. final 参数

    • 方法的参数也可以用 final 声明,这意味着在方法中你不能更改参数的引用。

    • 示例:

      1
      2
      3
      4
      public void display(final String message) {
      // message = "New Message"; // 错误:不能更改 `final` 参数
      System.out.println(message);
      }
  4. final 和多线程安全性

    • 在多线程环境中,final 变量可以确保变量在对象构造完成后被安全地发布给其他线程,因为 final 字段在对象构造完成后是可见的。

总结

  • final 类不能被继承。
  • final 方法不能被重写。
  • final 变量的值在初始化后不能被更改。
  • final 是创建不可变类和提供线程安全的一种常见技术。

抽象类

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

语法:访问修饰符 abstract 类名{}

  • 用abstract关键字来修饰一个方法的时候,该方法就是抽象方法

语法:访问修饰符 abstract 返回类型 方法名(参数列表);//没有方法体

有以下几点需要注意:

  • 抽象类不能够被实例化
  • 抽象类可以没有abstract方法
  • 一旦类被声明为abstract方法,则这个类必须声明为abstract
  • abstract只能够修饰类和方法,不能修饰属性和其他的
  • 一个类继承了抽象类,则它必须实现抽象类中的所有抽象方法,除非它自己也声明为abstract类
  • 抽象方法不能够用private,final和static来修饰,这些关键字都是和重写相违背的

接口

基础

接口就是给出一些没有实现的方法,封装到一起,在根据具体情况把这些方法写出来

语法如下:

1
2
3
4
5
6
7
8
9
interface 接口名{
//属性
//抽象方法
}
class 类名 implements 接口{
自己属性;
自己方法;
必须实现的接口的抽象方法
}

那么什么时候使用接口呢,下面举个例子:现在要制造战斗机,武装直升机,专家只需要将飞机所需的功能/规格定下来即可,剩下的交给别人实现就可以了、

下面来个简单的代码例子:
DBInterface.java

1
2
3
4
5
6
package show;

public interface DBInterface { //项目经理
public void connect();//连接方法
public void close();//关闭连接
}

MysqlDB.java

1
2
3
4
5
6
7
8
9
10
11
12
package show;

public class MysqlDB implements DBInterface{
@Override
public void connect() {
System.out.println("Connecting to MysqlDB");
}
@Override
public void close(){
System.out.println("Closing MysqlDB");
}
}

New.java

1
2
3
4
5
6
7
8
9
import show.MysqlDB;

public class New {
public static void main(String[] args) {
MysqlDB mysqlDB = new MysqlDB();
mysqlDB.connect();
mysqlDB.close();
}
}

有以下几点需要注意的

image-20240909214702343

image-20240909214608475

接口的多态特性

  • 多态参数:接口的多态参数指的是在方法定义中,使用接口类型作为参数。由于Java中接口可以被多个类实现,因此在方法中可以传入任何实现了该接口的类的对象,从而实现了多态性

在Java中,父类(或接口)类型的引用可以指向任何子类(或实现类)的对象,并且调用方法时,会根据对象的实际类型执行对应的方法

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
// 定义一个接口
interface Animal {
void sound();
}
// 实现接口的第一个类
class Dog implements Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
// 实现接口的第二个类
class Cat implements Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
// 一个使用接口作为参数的方法
class Zoo {
public void makeSound(Animal animal) {
animal.sound();// 动态绑定到具体对象的实现
}
}
public class Main {
public static void main(String[] args) {
Zoo zoo = new Zoo();
Dog dog = new Dog();
Cat cat = new Cat();
zoo.makeSound(dog); // 输出 "Dog barks"
zoo.makeSound(cat); // 输出 "Cat meows"
}
}
  • 多态数组
  • 接口存在多态传递现象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InterfacePolyPass {
public static void main(String[] args) {
//接口类型的变量可以指向实现了该接口的类的对象实例
IG ig = new Teacher();
//如果 IG 继承了 IH 接口,而 Teacher 类实现了 IG 接口
//那么,实际上就相当于 Teacher 类也实现了 IH 接口. //这就是所谓的 接口多态传递现象. IH ih = new Teacher();
}
}
interface IH {
void hi();
}
interface IG extends IH{ }
class Teacher implements IG {
@Override
public void hi() {
}
}

内部类

基础

一个类内部完整地嵌套另一个类,被嵌套的类称为内部类,嵌套其他类的类称为外部类

内部类的最大特点就是可以直接访问私有属性

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

举个例子,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Outer { //外部类
private int n1 = 100;//属性
public Outer(int n1) {//构造器
this.n1 = n1;
}
public void m1() {//方法
System.out.println("m1()");
}
{//代码块
System.out.println("代码块...");
}
class Inner { //内部类, 在 Outer 类的内部
}
}

内部类的分类

定义在外部类局部位置上(比如方法内)

局部内部类

  1. 定义在外部类的局部位置,比如方法中,并且有类名
  2. 可以直接访问所有成员,包括私有成员
  3. 不能添加访问修饰符,因为它其实就是一个局部变量,但可以使用final来修饰
  4. 仅仅作用于定义它的方法或代码块中
  5. 外部类访问内部类的成员的话要创建对象来访问
  6. 外部其他类不能访问局部内部类
  7. 如果外部类和局部内部类的成员重名时,遵守就近原则,如果想访问外部类的成员的话,可以用外部类名.this.成员来访问
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
public class LocalInnerClass {//
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.m1();
System.out.println("outer02 的 hashcode=" + outer02);
}
}
class Outer02 {//外部类
private int n1 = 100;
private void m2() {
System.out.println("Outer02 m2()");
}//私有方法
public void m1() {//方法
//1.局部内部类是定义在外部类的局部位置,通常在方法
//3.不能添加访问修饰符,但是可以使用 final 修饰
//4.作用域 : 仅仅在定义它的方法或代码块中
final class Inner02 {//局部内部类(本质仍然是一个类)
//2.可以直接访问外部类的所有成员,包含私有的
private int n1 = 800;
public void f1() {
//5. 局部内部类可以直接访问外部类的成员,比如下面 外部类 n1 和 m2()
//7. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,
// 使用 外部类名.this.成员)去访问
// Outer02.this 本质就是外部类的对象, 即哪个对象调用了 m1, Outer02.this 就是哪个对象
System.out.println("n1=" + n1 + " 外部类的 n1=" + Outer02.this.n1);
System.out.println("Outer02.this hashcode=" + Outer02.this);
m2();
}
}
//6. 外部类在方法中,可以创建 Inner02 对象,然后调用方法即可
Inner02 inner02 = new Inner02();
inner02.f1();
}
}

匿名内部类的使用(重要)

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

1
2
3
new 类或接口(参数列表){
类体
};

匿名内部类的特点:

  1. 没有名字:匿名内部类没有名字,不能在其他地方引用它,只能在声明时使用。
  2. 通常用于简化代码:特别是当你只需要一个类的一个实例,并且该类不需要被重复使用时,可以用匿名内部类。它通常用来实现接口或者继承类,以提供特定的实现。
  3. 只能创建一次实例:匿名内部类的创建与实例化是在同一个地方完成的,因此不能在多个地方重复使用该类。
  4. 语法简洁:匿名内部类允许在代码中定义一个类的同时创建一个该类的实例,省去了为类单独命名和定义的麻烦

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
// 创建一个线程,使用匿名内部类来实现Runnable接口
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("线程正在运行...");
}
};

Thread thread = new Thread(runnable);//创建一个新的线程对象
thread.start();
}
}

再举一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class New {
public static void main(String[] args) {
//当做实参直接传递,简洁高效
f1(new IL() {
@Override
public void show() {
System.out.println("这是一副名画~~...");
}
});
//传统方法
f1(new Picture());
}
//静态方法,形参是接口类型
public static void f1(IL il) {
il.show();
}
}
//接口
interface IL {
void show();
}
//类->实现 IL => 编程领域 (硬编码)
class Picture implements IL {
@Override
public void show() {
System.out.println("这是一副名画 XX...");
}
}

成员内部类

  1. 定义在外部类的成员位置上,并且没有static修饰
  2. 可以直接访问外部类的所有成员,包括私有的
  3. 可以添加任意访问修饰符,因为它的地位就是一个成员
  4. 作用域和外部类的其他成员一样,都为整个类体
  5. 当外部类和内部类重名的时候,内部类访问时遵守就近原则,想访问外部类成员,可以使用外部类名.this.成员来访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class New {
public static void main(String[] args) {
Outer08 outer08 = new Outer08();
outer08.t1();
//外部其他类,使用成员内部类的三种方式
// 第一种方式
// outer08.new Inner08(); 相当于把 new Inner08()当做是 outer08 成员
// 这就是一个语法,不要特别的纠结.
Outer08.Inner08 inner08 = outer08.new Inner08();
inner08.say();
// 第二方式 在外部类中,编写一个方法,可以返回 Inner08 对象
Outer08.Inner08 inner08Instance = outer08.getInner08Instance();
inner08Instance.say();
}
}
class Outer08 { //外部类
private int n1 = 10;
public String name = "张三";
private void hi() {
System.out.println("hi()方法...");
}
//1.注意: 成员内部类,是定义在外部内的成员位置上
//2.可以添加任意访问修饰符(public、protected 、默认、private),因为它的地位就是一个成员
public class Inner08 {//成员内部类
private double sal = 99.8;
private int n1 = 66;
public void say() {
//可以直接访问外部类的所有成员,包含私有的
//如果成员内部类的成员和外部类的成员重名,会遵守就近原则. //,可以通过 外部类名.this.属性 来访问外部类的成员
System.out.println("n1 = " + n1 + " name = " + name + " 外部类的 n1=" + Outer08.this.n1);
hi();
}
}
//方法,返回一个 Inner08 实例
public Inner08 getInner08Instance(){
return new Inner08();
}
//写方法
public void t1() {
//使用成员内部类
//创建成员内部类的对象,然后使用相关的方法
Inner08 inner08 = new Inner08();
inner08.say();
System.out.println(inner08.sal);
}
}

静态内部类

  1. 定义在外部类成员位置,并有static修饰
  2. 可以直接访问外部类所有的静态成员,包含私有的,但不能直接访问非静态成员
  3. 当外部类和静态内部类重名时,静态内部类访问时,遵循就近原则,如果想访问外部类的成员,则可以使用外部类名.成员来访问

枚举

基础

Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。

例如定义一个颜色的枚举类。

1
2
3
4
enum Color 
{
RED, GREEN, BLUE;
}

以上枚举类 Color 颜色常量有 RED, GREEN, BLUE,分别表示红色,绿色,蓝色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Color 
{
RED, GREEN, BLUE;
}

public class Test
{
// 执行输出结果
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1);
}
}

内部类中使用枚举

枚举类也可以声明在内部类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test 
{
enum Color
{
RED, GREEN, BLUE;
}
// 执行输出结果
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1);
}
}

每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。

以上的枚举类 Color 转化在内部类实现:

1
2
3
4
5
6
class Color
{
public static final Color RED = new Color();
public static final Color BLUE = new Color();
public static final Color GREEN = new Color();
}

迭代枚举元素

可以使用 for 语句来迭代枚举元素:

1
2
3
4
5
6
7
8
9
10
11
enum Color 
{
RED, GREEN, BLUE;
}
public class MyClass {
public static void main(String[] args) {
for (Color myVar : Color.values()) {
System.out.println(myVar);
}//Color.values() 是一个静态方法,返回枚举类型中所有定义的常量值(即 RED、GREEN 和 BLUE)作为一个数组
}
}

在 switch 中使用枚举类

枚举类常应用于 switch 语句中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Color 
{
RED, GREEN, BLUE;
}
public class MyClass {
public static void main(String[] args) {
Color myVar = Color.BLUE;

switch(myVar) {
case RED:
System.out.println("红色");
break;
case GREEN:
System.out.println("绿色");
break;
case BLUE:
System.out.println("蓝色");
break;
}
}
}

values(), ordinal() 和 valueOf() 方法

enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Serializable 和 java.lang.Comparable 两个接口。

values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:

  • values() 返回枚举类中所有的值。
  • ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
  • valueOf()方法返回指定字符串值的枚举常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Color 
{
RED, GREEN, BLUE;
}

public class Test
{
public static void main(String[] args)
{
// 调用 values()
Color[] arr = Color.values();

// 迭代枚举
for (Color col : arr)
{
// 查看索引
System.out.println(col + " at index " + col.ordinal());
}

// 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
System.out.println(Color.valueOf("RED"));
// System.out.println(Color.valueOf("WHITE"));
}
}

枚举类成员

枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用

枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它

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
enum Color 
{
RED, GREEN, BLUE;

// 构造函数
private Color()
{
System.out.println("Constructor called for : " + this.toString());
}

public void colorInfo()
{
System.out.println("Universal Color");
}
}

public class Test
{
// 输出
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1);
c1.colorInfo();
}
}

集合

Java 的集合类主要分为两大类

1.Collection 接口的子类(单列集合):是 Java 集合框架中的根接口,主要用于存储一组元素

常见子接口包括:

List(列表):有序且允许重复的集合,常用实现类包括:

  • ArrayList
  • LinkedList
  • Vector

Set(集合):无序且不允许重复的集合,常用实现类包括:

  • HashSet
  • LinkedHashSet
  • TreeSet

Queue(队列):用于按顺序处理元素,常用实现类包括:

  • LinkedList
  • PriorityQueue

2.Map 接口的子类(双列集合):主要用于存储键值对

Collection接口和常用方法

基本操作

Collection 定义了集合对象的基本操作,例如添加、删除、遍历、清空等。这些方法被子类(如 ListSet 等)继承并具体实现。主要方法包括:

  • add(E e):向集合中添加元素。
  • remove(Object o):从集合中移除指定元素。
  • contains(Object o):判断集合中是否包含指定元素。
  • size():返回集合中元素的个数。
  • isEmpty():判断集合是否为空。
  • clear():清空集合中的所有元素。
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
import java.util.ArrayList;
import java.util.List;

public class New {
//忽略所有类型的警告
@SuppressWarnings({"all"})
public static void main (String []args)
{
List list = new ArrayList();
// add:添加单个元素
list.add("jack");
list.add(10);//list.add(new Integer(10))
list.add(true);
System.out.println("list=" + list);
// remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
// contains:查找元素是否存在
System.out.println(list.contains("jack"));//T
// size:获取元素个数
System.out.println(list.size());//2
// isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
// clear:清空
list.clear();
System.out.println("list=" + list);
// addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("list=" + list);
// containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));//T
// removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println("list=" + list);//[聊斋]
// 说明:以 ArrayList 实现类来演示.
}
}

用 Iterator(迭代器)遍历接口元素

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

通常,Iteratorwhile 循环一起使用,通过其方法来进行遍历。

迭代器的工作原理

  1. 创建迭代器:调用集合对象的 iterator() 方法来创建一个 Iterator 实例。这个迭代器指向集合的第一个元素之前的位置。
  2. 遍历元素:通过 hasNext() 来检查是否有下一个元素,通过 next() 来获取下一个元素的值。内部会记录当前位置,每调用一次 next(),迭代器会移动到下一个元素。
  3. 删除元素(可选):如果需要,可以调用 remove() 方法来删除刚刚通过 next() 访问的元素

Iterator 接口包含以下三个核心方法:

  1. boolean hasNext(): 检查集合中是否还有下一个元素。如果存在下一个元素,则返回 true,否则返回 false

    1
    2
    3
    if (iterator.hasNext()) {
    // Do something
    }
  2. E next(): 返回集合中的下一个元素。如果调用时没有元素,通常会抛出 NoSuchElementException

    1
    E element = iterator.next();
  3. void remove(): 从底层集合中移除 next() 方法返回的上一个元素。这个方法是可选的,如果集合不支持元素的删除操作,可能会抛出 UnsupportedOperationException

来个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");

Iterator<String> iterator = list.iterator();

while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}

特点

  • Iterator 提供了一种统一的方式来遍历不同类型的集合。
  • 它只支持单向遍历,不允许回退。(想再次遍历需要重置迭代器,即iterator = col.iterator();
  • 与增强的 for 循环相比,Iterator 提供了更细粒度的控制(如删除操作)

增强for循环

增强for循环,可以替代iterator迭代器

特点:其实就是简单版的iterator,本质一样,只能用于遍历集合或者数组

基本语法:

1
2
3
for(元素类型 元素名:集合名或数组名){
访问元素
}

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class New {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("小黑", 3));
list.add(new Dog("大黄", 100));
list.add(new Dog("大壮", 8));
//先使用 for 增强
for (Object dog : list) {
System.out.println("dog=" + dog);
}
//使用迭代器
System.out.println("===使用迭代器来遍历===");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object dog = iterator.next();
System.out.println("dog=" + dog);
}
}
}

/**
* 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
* 用迭代器和增强 for 循环两种方式来遍历
* 重写 Dog 的 toString 方法, 输出 name 和 age
*/
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

List接口和常用方法

基本

  • List接口是Collection接口的子接口
  • List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  • List集合中的每个元素都有其对应的顺序索引
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

常用方法

以下是 List 接口中的一些常用方法:

  1. void add(E element)
  • 功能: 将指定的元素添加到列表的末尾。

  • 示例

    1
    2
    List<String> list = new ArrayList<>();
    list.add("Apple");
  1. void add(int index, E element)
  • 功能: 在指定位置插入元素。其后的元素将向右移动。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.add(0, "Banana"); // 在索引 0 位置插入元素
  1. E get(int index)
  • 功能: 返回列表中指定索引处的元素。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    String fruit = list.get(0); // 获取索引 0 处的元素
  1. E set(int index, E element)
  • 功能: 用指定的元素替换列表中指定索引位置的元素,并返回被替换的元素。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.set(0, "Banana"); // 将索引 0 处的元素替换为 Banana
  1. E remove(int index)
  • 功能: 移除指定索引位置的元素,并返回被移除的元素。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.remove(0); // 移除索引 0 处的元素
  1. boolean remove(Object o)
  • 功能: 从列表中移除首次出现的指定元素。如果列表中包含该元素,返回 true;否则,返回 false

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.remove("Apple"); // 移除元素 "Apple"
  1. int size()
  • 功能: 返回列表中的元素个数。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    int size = list.size(); // 返回列表的大小
  1. boolean isEmpty()
  • 功能: 如果列表不包含元素,则返回 true;否则,返回 false

  • 示例

    1
    2
    List<String> list = new ArrayList<>();
    boolean isEmpty = list.isEmpty(); // 检查列表是否为空
  1. boolean contains(Object o)
  • 功能: 如果列表中包含指定的元素,则返回 true

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    boolean contains = list.contains("Apple"); // 检查是否包含 "Apple"
  1. int indexOf(Object o)
  • 功能: 返回列表中第一次出现的指定元素的索引,如果列表中不包含该元素,则返回 -1

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    int index = list.indexOf("Apple"); // 返回元素 "Apple" 的索引
  1. int lastIndexOf(Object o)
  • 功能: 返回列表中最后一次出现的指定元素的索引,如果列表中不包含该元素,则返回 -1

  • 示例

    1
    2
    3
    4
    5
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.add("Banana");
    list.add("Apple");
    int lastIndex = list.lastIndexOf("Apple"); // 返回 "Apple" 最后出现的索引
  1. void clear()
  • 功能: 移除列表中的所有元素。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.clear(); // 清空列表
  1. List<E> subList(int fromIndex, int toIndex)
  • 功能: 返回列表中指定范围内的元素的视图(包含 fromIndex,不包含 toIndex)。

  • 示例

    1
    2
    3
    4
    5
    List<String> list = new ArrayList<>();
    list.add("Apple");
    list.add("Banana");
    list.add("Cherry");
    List<String> sublist = list.subList(0, 2); // 返回子列表 ["Apple", "Banana"]
  1. Object[] toArray()
  • 功能: 返回包含列表中所有元素的数组。

  • 示例

    1
    2
    3
    List<String> list = new ArrayList<>();
    list.add("Apple");
    Object[] array = list.toArray(); // 转换为数组
  1. boolean addAll(int index, Collection eles)
  • 功能: 从 index 位置开始将 eles 中的所有元素添加进来

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    List<String> list1 = new ArrayList<>();
    list1.add("Apple");
    list1.add("Banana");
    List<String> list2 = new ArrayList<>();
    list2.add("Cherry");
    list2.add("Date");
    list1.addAll(1, list2); // 在索引 1 处插入 list2 的所有元素
    System.out.println(list1);

LinkedList 底层结构

基本

1.双向链表LinkedList 由节点组成,每个节点包含三部分:一个元素值、指向前一个节点的引用(prev)、以及指向下一个节点的引用(next)。

2.动态扩展:与数组不同,LinkedList 不需要预先定义大小,它可以根据需要动态扩展,方便在中间进行元素插入和删除操作。

3.实现接口

  • List 接口LinkedList 可以当作一个列表来使用,支持按索引访问元素,提供列表的所有基本操作,比如增加、删除、获取元素等。
  • Deque 接口LinkedList 可以当作双端队列使用,支持从两端插入和删除元素,这使得它非常适合用作队列(FIFO)或栈(LIFO)。

常用方法:

  • 添加元素
    • add(E element):将元素添加到列表末尾。
    • addFirst(E element):在链表开头添加元素。
    • addLast(E element):在链表末尾添加元素(与 add() 相同)。
  • 删除元素
    • remove():移除并返回链表的第一个元素。
    • removeFirst():移除并返回链表的第一个元素(与 remove() 相同)。
    • removeLast():移除并返回链表的最后一个元素。
    • remove(int index):删除链表中指定索引位置的元素,并返回被删除的元素。
    • remove(Object o):删除链表中首次出现的指定对象
  • 访问元素
    • get(int index):获取指定位置的元素。
    • getFirst():获取第一个元素。
    • getLast():获取最后一个元素

举个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.LinkedList;

public class Main {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();

// 添加元素
list.add("A");
list.add("B");
list.addFirst("Start");
list.addLast("End");

// 访问元素
System.out.println("First element: " + list.getFirst()); // Start
System.out.println("Last element: " + list.getLast()); // End

// 删除元素
list.removeFirst(); // 删除第一个元素
list.removeLast(); // 删除最后一个元素

System.out.println("List after removals: " + list);
}
}

LinkedList vs. ArrayList:

  • ArrayList 更适合随机访问元素,因为它是基于数组实现的,可以通过索引快速访问元素。
  • LinkedList 更适合频繁的插入和删除操作,因为它不需要移动大量数据

Set接口和常用方法

基本

无序,即添加和取出的顺序不一致,没有索引

不允许重复元素,所以最多包含一个null

常用方法

Set接口为Collection接口的子接口,所以常用方法和Colletion接口一样

遍历方式

  1. 可以使用迭代器
  2. 增强for循环
  3. 不能使用索引的方式来获取

举个例子如下所示:

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
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class New {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");//重复
set.add("jack");
set.add("hsp");
set.add("mary");
set.add(null);//
set.add(null);//再次添加 null
for(int i = 0; i <10;i ++) {
System.out.println("set=" + set);
}
//遍历
//方式 1: 使用迭代器
System.out.println("=====使用迭代器====");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
set.remove(null);
//方式 2: 增强 for
System.out.println("=====增强 for====");
for (Object o : set) {
System.out.println("o=" + o);
}
//set 接口对象,不能通过索引来获取
}
}

Map接口和常用方法

特点

  • Map 是用于存储键值对(key-value pairs)的集合,每个键唯一对应一个值,即可以通过指定的key找到对应的value

  • 不能包含重复的键,但可以包含重复的值。

  • 常见实现包括 HashMapTreeMapLinkedHashMap,它们分别有不同的排序和性能特性

  • 当有相同的key时,就相当于等价替换:

    1
    2
    map.put("no1", "韩顺平");//k-v
    map.put("no1", "张三丰");//等价替换

常用方法

添加和修改元素

  • put(K key, V value):将指定的键值对添加到 Map 中。如果键已存在,则更新其对应的值。
1
map.put("key1", "value1");
  • putAll(Map<? extends K, ? extends V> m):将指定 Map 中的所有键值对添加到当前 Map 中。

删除元素

  • remove(Object key):移除指定键的键值对,并返回其对应的值。
1
String removedValue = map.remove("key1");
  • clear():清空 Map,移除所有键值对。

    1
    map.clear();

查询元素

  • get(Object key):返回指定键对应的值;如果 Map 中不存在该键,则返回 null
1
String value = map.get("key1");
  • containsKey(Object key):检查 Map 是否包含指定的键。
1
boolean hasKey = map.containsKey("key1");
  • containsValue(Object value):检查 Map 是否包含指定的值。
1
boolean hasValue = map.containsValue("value1");

获取 Map 的信息

  • size():返回 Map 中的键值对数量。
1
int size = map.size();
  • isEmpty():判断 Map 是否为空。
1
boolean isEmpty = map.isEmpty();

遍历方法

  • **keySet()**:返回映射中所有键的集合。
  • **values()**:返回映射中所有值的集合。
  • **entrySet()**:返回映射中所有键值对的集合(Map.Entry 对象的集合)。可以通过它来遍历 Map 的键值对

例子如下:

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
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class New {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap();
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);

// 使用 keySet 遍历键
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key + " = " + map.get(key));
}
// 使用 entrySet 遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 使用 values 遍历值
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println(value);
}
}
}

开发中如何选择集合实现类

image-20240919164728799

Collections工具类

Collections是一个操作Set、List和Map等集合的工具类

Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

以下是一些常用的 Collections 方法:

  1. 排序Collections.sort(List<T> list) 可以对 List 进行升序排序。

    1
    2
    3
    List<Integer> numbers = Arrays.asList(5, 3, 9, 1);
    Collections.sort(numbers);
    System.out.println(numbers); // 输出 [1, 3, 5, 9]
  2. 查找最大值和最小值

    • Collections.max(Collection<T> coll) 查找集合中的最大值。
    • Collections.min(Collection<T> coll) 查找集合中的最小值。
    1
    2
    int max = Collections.max(numbers);  // 返回 9
    int min = Collections.min(numbers); // 返回 1
  3. 反转Collections.reverse(List<?> list) 可以将 List 反转。

    1
    2
    Collections.reverse(numbers);
    System.out.println(numbers); // 输出 [9, 5, 3, 1]
  4. 随机打乱Collections.shuffle(List<?> list) 可以将 List 随机打乱顺序。

    1
    2
    Collections.shuffle(numbers);
    System.out.println(numbers); // 输出打乱后的顺序
  5. 不可修改集合Collections.unmodifiableList(List<? extends T> list) 可以返回一个不可修改的集合。

    1
    2
    List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
    // 试图修改 unmodifiableList 会抛出 UnsupportedOperationException

泛型

基础

Java中的泛型(Generics)是Java 5引入的一项重要特性,允许类、接口和方法能够操作指定类型的对象,而无需在编写代码时明确指定类型。这种机制通过将类型参数化,提供了更高的代码复用性和类型安全性

传统方法面临的问题

  • 不能对加入到集合中的数据类型进行约束(比如要求输入的是Dog类,结果不小心输入了Cat类)

  • 遍历的时候,需要进行类型转换,影响效率

    1
    2
    3
    4
    5
    for (Object o : arrayList) {
    //向下转型 Object ->Dog
    Dog dog = (Dog) o;
    System.out.println(dog.getName() + "-" + dog.getAge());
    }

对于泛型举个例子如下:

  1. 当我们 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是 Dog 类型 (细节后面说…)

  2. 如果编译器发现添加的类型,不满足要求,就会报错

  3. 在遍历的时候,可以直接取出 Dog 类型而不是 Object

    1
    2
    3
    for (Dog dog : arrayList) {
    System.out.println(dog.getName() + "-" + dog.getAge());
    }
  4. public class ArrayList<E> {} E 称为泛型,那么 Dog->E

泛型的优点:

  • 编译时检查了添加元素的类型,提高了安全性
  • 减少了类型转换的次数,提高了效率
  • 不会提示编译警告

利用

1. 泛型类

定义一个泛型类 Box,用来存储任意类型的对象:

1
2
3
4
5
6
7
8
9
10
11
public class Box<T> {
private T item;

public void setItem(T item) {
this.item = item;
}

public T getItem() {
return item;
}
}

使用泛型类的例子

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println("String: " + stringBox.getItem());

Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println("Integer: " + integerBox.getItem());
}
}

输出

1
2
String: Hello
Integer: 123

2. 泛型方法

定义一个泛型方法 printArray,可以打印任何类型数组的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.println(element);
}
}

public static void main(String[] args) {
Integer[] intArray = {1, 2, 3};
String[] strArray = {"Hello", "World"};

printArray(intArray); // 打印整型数组
printArray(strArray); // 打印字符串数组
}
}

输出

1
2
3
4
5
1
2
3
Hello
World

3. 泛型接口

定义一个泛型接口 Container,用来存储和获取任意类型的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Container<T> {
void add(T item);
T get();
}

public class StringContainer implements Container<String> {
private String item;

@Override
public void add(String item) {
this.item = item;
}

@Override
public String get() {
return item;
}
}

使用泛型接口的例子

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
StringContainer container = new StringContainer();
container.add("Hello Generics");
System.out.println(container.get());
}
}

输出

1
Hello Generics

4. 泛型通配符

使用通配符 <? extends T><? super T> 来限定类型的范围:

上限通配符 <? extends T>

表示类型必须是T或其子类。定义一个方法打印所有类型为Number及其子类的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.List;

public class Main {
public static void printNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number);
}
}

public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);

printNumbers(intList); // 打印整型列表
printNumbers(doubleList); // 打印双精度列表
}
}

输出

1
2
3
4
5
6
1
2
3
1.1
2.2
3.3

下限通配符 <? super T>

表示类型必须是T或其父类。定义一个方法将元素添加到列表中,列表可以是Integer或其父类的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.List;
import java.util.ArrayList;

public class Main {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}

public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);

for (Object num : numberList) {
System.out.println(num);
}
}
}

输出

1
2
3
1
2
3

总结

  • 泛型类:可以让类存储和操作多种不同类型的数据。
  • 泛型方法:允许方法适应多种类型。
  • 泛型接口:使接口可以与不同类型的实现类进行交互。
  • 通配符:提供了更灵活的类型限定。

常见泛型符号:

  • T:Type,表示类型。
  • E:Element,常用于集合类中。
  • K:Key,常用于键值对。
  • V:Value,常用于键值对。

注意事项:

  • T、E只能是引用类型
  • 在给泛型指定具体类型后,可以传入该类型或者其子类类型
  • 当写List list3 = new ArrayList();,默认它的泛型为E,也就是Object

自定义泛型

自定义泛型类

基本语法

1
2
3
4
class 类名 <T,R...>//...表示可以有多个泛型
{
成员
}

注意事项:

  • 普通成员可以使用泛型
  • 使用泛型的数组,不能初始化
  • 静态方法中不能使用类的泛型
  • 泛型类的类型,是在创建对象的时候确定的(因为创建对象时,需要指定类型)
  • 如果在创建对象的时候没有指定类型,默认为Object

下面举个相关的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.util.Arrays;

@SuppressWarnings({"all"})
public class New {
public static void main(String[] args) {
//T=Double, R=String, M=Integer
Tiger<Double,String,Integer> g = new Tiger<>("john");
g.setT(10.9); //OK
//g.setT("yy"); //错误,类型不对
System.out.println(g);
Tiger g2 = new Tiger("john~~");//OK T=Object R=Object M=Object
g2.setT("yy"); //OK ,因为 T=Object "yy"=String 是 Object 子类
System.out.println("g2=" + g2);
}
}

//1. Tiger 后面泛型,所以我们把 Tiger 就称为自定义泛型类
//2, T, R, M 泛型的标识符, 一般是单个大写字母
//3. 泛型标识符可以有多个. //4. 普通成员可以使用泛型 (属性、方法)
//5. 使用泛型的数组,不能初始化
//6. 静态方法中不能使用类的泛型
class Tiger<T, R, M> {
String name;
R r; //属性使用到泛型
M m;
T t;
//因为数组在 new 不能确定 T 的类型,就无法在内存开空间
T[] ts;
public Tiger(String name) {
this.name = name;
}
public Tiger(R r, M m, T t) {//构造器使用泛型
this.r = r;
this.m = m;
this.t = t;
}
public Tiger(String name, R r, M m, T t) {//构造器使用泛型
this.name = name;
this.r = r;
this.m = m;
this.t = t;
}
//因为静态是和类相关的,在类加载时,对象还没有创建
//所以,如果静态方法和静态属性使用了泛型,JVM 就无法完成初始化
// static R r2;
// public static void m1(M m) {
//
// }
//方法使用泛型
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public R getR() {
return r;
}
public void setR(R r) {//方法使用到泛型
this.r = r;
}
public M getM() {//返回类型可以使用泛型. return m;
return m;
}
public void setM(M m) {
this.m = m;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
@Override
public String toString() {
return "Tiger{" +
"name='" + name + '\'' +
", r=" + r +
", m=" + m +
", t=" + t +
", ts=" + Arrays.toString(ts) +
'}';
}
}

自定义泛型接口

基本语法:

1
2
interface 接口名<T,R>{
}

注意事项:

  • 接口中,静态成员不能使用泛型
  • 泛型接口的类型,在继承接口或者实现接口的时候确定
  • 没有指定类型,默认为Object

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//在继承接口 指定泛型接口的类型
interface IA extends IUsb<String, Double> {
}
//当我们去实现 IA 接口时,因为 IA 在继承 IUsu 接口时,指定了 U 为 String R 为 Double
//,在实现 IUsu 接口的方法时,使用 String 替换 U, 是 Double 替换 R
class AA implements IA {
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double aDouble) {
}
@Override
public void run(Double r1, Double r2, String u1, String u2) {
}
}
//实现接口时,直接指定泛型接口的类型
//给 U 指定 Integer 给 R 指定了 Float
//所以,当我们实现 IUsb 方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float aFloat) {
}
@Override
public void run(Float r1, Float r2, Integer u1, Integer u2) {
}
}
//没有指定类型,默认为 Object
//建议直接写成 IUsb<Object,Object>
class CC implements IUsb { //等价 class CC implements IUsb<Object,Object> {
@Override
public Object get(Object o) {
return null;
}
@Override
public void hi(Object o) {
}
@Override
public void run(Object r1, Object r2, Object u1, Object u2) {
}
}
interface IUsb<U, R> {
int n = 10;
//U name; 不能这样使用
//普通方法中,可以使用接口泛型
R get(U u);
void hi(R r);
void run(R r1, R r2, U u1, U u2);
//在 jdk8 中,可以在接口中,使用默认方法, 也是可以使用泛型
default R method(U u) {
return null;
}
}

自定义泛型方法

基本语法:

1
2
修饰符 <T,R>返回类型 方法名(参数列表){
}

注意事项:

  • 泛型方法可以定义在普通类中,也可以定义在泛型类中
  • 当泛型方法被调用时,类型就会确定
  • public void eat(E e){},修饰符后没有<T,R…>eat方法不是泛型方法,而是使用了泛型

举个例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.ArrayList;

@SuppressWarnings({"all"})
public class New {
public static void main(String[] args) {
Car car = new Car();
car.fly("宝马", 100);//当调用方法时,传入参数,编译器,就会确定类型
System.out.println("=======");
car.fly(300, 100.1);//当调用方法时,传入参数,编译器,就会确定类型
//测试
//T->String, R-> ArrayList
Fish<String, ArrayList> fish = new Fish<>();
fish.hello(new ArrayList(), 11.3f);
}
}

//泛型方法,可以定义在普通类中, 也可以定义在泛型类中
class Car {//普通类
public void run() {//普通方法
}
//说明 泛型方法
//1. <T,R> 就是泛型
//2. 是提供给 fly 使用的
public <T, R> void fly(T t, R r) {//泛型方法
System.out.println(t.getClass());//String
System.out.println(r.getClass());//Integer
}
}
class Fish<T, R> {//泛型类

public void run() {//普通方法
}

public <U, M> void eat(U u, M m) {//泛型方法
}

//说明
//1. 下面 hi 方法不是泛型方法
//2. 是 hi 方法使用了类声明的 泛型
public void hi(T t) {
}

//泛型方法,可以使用类声明的泛型,也可以使用自己声明泛型
public <K> void hello(R r, K k) {
System.out.println(r.getClass());//ArrayList
System.out.println(k.getClass());//Float
}
}