java学习
java学习
Sherlock面向对象-基础
类和对象的内存分配机制
Java 内存的结构分析
- 栈: 一般存放基本数据类型(局部变量)
- 堆: 存放对象(Cat cat , 数组等)
- 方法区:常量池(常量,比如字符串), 类加载信息
举一个创建对象的流程分析例子
1 | Person p = new Person(); |
- 先加载 Person 类信息(属性和方法信息, 只会加载一次)
- 在堆中分配空间, 进行默认初始化(看规则)
- 把地址赋给 p , p 就指向对象
- 进行指定初始化, 比如 p.name =”jack” p.age = 10
成员方法调用机制示意图
递归
例题一
下面为该题代码
1 | public class Test { |
汉诺塔
1 | public class StringToBasicDetail { |
可变参数
java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法
基本语法如下所示:
1 | 访问修饰符 返回类型 方法名(数据类型... 形参名) { |
举个例子:
1 | public class StringToBasicDetail { |
可变参数与普通参数一起的例子如下:
1 | public class StringToBasicDetail { |
构造器
基础
构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。
它有几个特点:
- 方法名和类名相同
- 没有返回值
- 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化
1 | class Person { |
一个类也可以有多个构造器,即构造器重载
创建对象的时候,会自动调用该类的构造方法
this关键字
this表示的就是当前对象
1 | public class StringToBasicDetail { |
面向对象-中级
包(package)
包(package)是用于组织类和接口的命名空间。包提供了一种将相关的类和接口进行分组的机制,方便代码的管理和维护。通过使用包,可以避免类名冲突,增强代码的可读性和可维护性
包的本质实际上就是创造出不同的文件夹来保存类文件
包的声明用package关键字,引入用import关键字,如下:
我们需要使用到哪个类,就导入哪个类即可,不建议使用 *
导入
1 | import java.util.Scanner; //表示只会引入 java.util 包下的 Scanner |
访问修饰符
java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):
- 公开级别:用 public 修饰,对外公开
- 受保护级别:用 protected 修饰,对子类和同一个包中的类公开
- 默认级别:没有修饰符号,向同一个包的类公开
- 私有级别:用 private 修饰,只有类本身可以访问,不对外公开
继承
- 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访 问,要通过父类提供公共的方法去访问
- 子类必须调用父类的构造器, 完成父类的初始化
- 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过
- 如果希望指定去调用父类的某个构造器,则显式的调用一下 :
super(参数列表)
- super 在使用时,必须放在构造器第一行(super只能在构造器中使用)
- super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
- java 所有类都是 Object 类的子类, Object 是所有类的基类.
- 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类)
- 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制
- 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
这里来解释一下super()和this()之间的区别所在
super()
- 调用父类构造方法:
super()
用于在子类的构造方法中调用父类的构造方法。这样做是为了确保父类的构造方法在子类的构造方法之前执行。 - 必须是构造方法的第一行:
super()
调用必须是子类构造方法中的第一条语句。如果没有显式地调用super()
,编译器会在没有参数的情况下隐式地调用父类的无参构造方法。 - 参数传递:可以使用参数将值传递给父类的构造方法,例如
super(param1, param2)
。
来个例子如下:
1 | class Parent { |
this()
- 调用当前类的构造方法:
this()
用于调用当前类的另一个构造方法。这通常用于构造方法之间的重用代码。 - 必须是构造方法的第一行:与
super()
类似,this()
调用也必须是构造方法中的第一条语句。 - 参数传递:可以使用参数将值传递给当前类的另一个构造方法,例如
this(param1, param2)
。
1 | class Example { |
super关键字
super 代表父类的引用,用于访问父类的属性、方法、构造器
super关键字不会访问当前类(本类)的成员和方法
访问父类的成员变量
1 | class Parent { |
访问父类的方法
1 | class Parent { |
super关键字所带来的便利
方法覆写
方法覆写就是子类中有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的方法
以下是几个需要注意的点:
方法签名:
- 覆写的方法必须具有与父类方法相同的方法签名,包括方法名和参数列表。
访问修饰符:
- 子类方法的访问修饰符不能比父类方法的更严格。例如,如果父类方法是
public
,子类方法也必须是public
。
返回类型:
- 覆写方法的返回类型可以是父类方法返回类型的子类型(协变返回类型),但不能是其他不相关的类型。(父类 返回类型是 Object ,子类方法返回类型是 String)
异常处理:
- 子类方法抛出的异常不能比父类方法抛出的异常更广泛。例如,如果父类方法抛出一个
IOException
,子类方法只能抛出IOException
或其子类的异常,不能抛出Exception
或其他更泛的异常。
使用@Override
注解:
- 在覆写的方法上加上
@Override
注解有助于编译器检查是否正确地覆写了父类的方法。如果没有正确覆写,编译器会报错。
调用父类方法:
- 在子类方法中,可以使用
super
关键字调用父类的方法。例如:super.methodName()
。
静态方法不能覆写:
- 静态方法不能被覆写(Override),而是被隐藏(Hide)。如果子类定义了一个与父类静态方法相同的方法,这只是隐藏了父类的方法,并不是覆写。
final
方法不能覆写:
- 如果父类的方法被声明为
final
,那么子类不能覆写这个方法。
构造方法不能覆写:
- 构造方法不能被继承或覆写。
1 | class Animal { |
重写与覆写之间的区别
下面来一个实例:
- 编写一个 Person 类,包括属性/private(name、age),构造器、方法 say(返回自我介绍的字符串)
- 编写一个 Student 类,继承 Person 类,增加 id、score 属性/private,以及构造器,定义 say 方法(返回自我介绍的信息)
- 在 main 中,分别创建 Person 和 Student 对象,调用 say 方法输出自我介绍
Person.java
1 | package show; |
Student.java
1 | package show; |
Test.java
1 | import show.*; |
多态
方法的多态
方法的重写和重载就体现了多态
对象的多态
类Dog,Cat都是类Animal的子类,已经定义过了
1 | public class PolyObject { |
因此,利用多态我们就可以完成下面这道编程题目
对于这道题,当我们写完各种的子类之后,在上面类Master中的方法就只要写下面这个
多态的注意事项和细节
多态的前提是:两个对象(类)存在继承关系
举个例子,如下所示:
1 | public class PolyDetail { |
属性没有重写一说,属性的值要看编译类型
1 | public class PolyDetail02 { |
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 | import show.*; |
(2)多态参数
方法定义的形参类型为父类类型,实参类型可以为子类类型
实例就比如前面的主人喂食动物
面向对象-高级
类变量和类方法
类变量
在 Java 中,类变量(也称为静态变量,static variable
)是属于整个类而不是某个特定对象的变量。类变量在类加载时初始化,并且在类的所有实例之间共享。它们使用关键字 static
来声明
定义类变量:访问修饰符 static 数据类型 变量名;
访问类变量:类名/对象名.类变量名;
来个例子简单了解一下
1 | public class New { |
注意
- 类变量在类加载的时候就初始化了,也就是说即使你没有创建对象,只要类加载了,就可以使用类变量了
- 当一个类的所有对象都共享一个变量的时候,考虑使用类变量:比如定义学生类,统计所有学生一共交了多少钱 Student(name,static fee)
类方法
类方法就是静态方法,形式为:访问修饰符 static 数据返回类型 方法名(){}
使用:类名/对象名.类方法名
来个例子理解一下
1 | public class New { |
注意:静态方法只能访问静态成员,非静态的方法可以访问静态成员和非静态成员
代码块
代码化块又称为初始化块,属于类中的成员,类似于方法,但和方法不一样的是它没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用
语法:[修饰符]{代码};
,要注意修饰符可选,要写的话也只能写static
代码块相当于另一种形式的构造器,可以进行初始化操作,如果说多个构造器中都有重复语句时,可以抽取到代码块中,提高代码重用性
举个例子如下:
1 | class Movie { |
static代码块也叫做静态代码块,作用是对类进行初始化,随类的加载而执行,并且只会执行一次,而普通的代码块没创建一个对象就执行
那么类什么时候会被加载呢
创建类的实例时:
当你使用
new
关键字创建一个类的对象时,JVM 会先检查该类是否已加载。如果没有加载,则会触发类的加载。示例:
1
MyClass obj = new MyClass(); // 创建对象,加载 MyClass 类
访问类的静态成员(静态变量或静态方法)时:
当你访问一个类的静态变量或调用静态方法时,如果类尚未加载,JVM 会加载该类。
示例:
1
2System.out.println(MyClass.staticVariable); // 访问静态变量,加载 MyClass 类
MyClass.staticMethod(); // 调用静态方法,加载 MyClass 类
类的初始化(静态代码块):
类在加载时,静态代码块会被执行。这通常发生在静态成员被访问或类被实例化之前。
示例:
1
2
3
4
5
6class MyClass {
static {
System.out.println("MyClass loaded");
}
}
// 任何访问 MyClass 的操作都会触发静态代码块的执行
子类初始化时:
当一个子类被初始化时,如果其父类尚未被加载,那么父类会先被加载。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class 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
关键字的用法
用于类(
final class
):含义:如果一个类被声明为
final
,则不能被继承。这意味着没有其他类可以扩展这个类。用途:使用
final
类通常是为了确保该类的实现不会被更改或扩展。例如,java.lang.String
类就是一个final
类,它不能被继承。示例
1
2
3public final class Constants {
// 该类不能被继承
}
用于方法(
final method
):含义:如果一个方法被声明为
final
,则子类不能重写这个方法。用途:使用
final
方法通常是为了防止子类更改方法的核心行为,确保方法的功能保持不变。示例
1
2
3
4
5
6
7
8
9
10
11
12class 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.");
// }
}
用于变量(
final variable
):含义:如果一个变量被声明为
final
,则它的值一旦被初始化,就不能再更改。用途
- 对于 基本数据类型(如
int
、char
等),final
变量的值在初始化后不可变。 - 对于 引用数据类型(如对象、数组),
final
变量的引用在初始化后不可更改(即不能指向另一个对象),但对象的内容可以改变。
- 对于 基本数据类型(如
示例
1
2
3
4
5final int MAX_VALUE = 100; // 基本数据类型,值不能更改
final Person person = new Person("John");
// person = new Person("Doe"); // 错误:不能改变引用
person.setName("Doe"); // 合法:对象内容可以改变
特别说明
final
和类加载的优化:- JVM 和编译器可以利用
final
关键字进行优化。例如,一个final
变量的值是已知的,可以在编译时内联(inlining),从而提高性能。
- JVM 和编译器可以利用
final
和不可变对象:- 使用
final
关键字是创建不可变对象的重要步骤之一。不可变对象的所有字段都应该是final
,以确保其状态在构造后不会改变。
- 使用
final
参数:方法的参数也可以用
final
声明,这意味着在方法中你不能更改参数的引用。示例:
1
2
3
4public void display(final String message) {
// message = "New Message"; // 错误:不能更改 `final` 参数
System.out.println(message);
}
final
和多线程安全性:- 在多线程环境中,
final
变量可以确保变量在对象构造完成后被安全地发布给其他线程,因为final
字段在对象构造完成后是可见的。
- 在多线程环境中,
总结
final
类不能被继承。final
方法不能被重写。final
变量的值在初始化后不能被更改。final
是创建不可变类和提供线程安全的一种常见技术。
抽象类
- 当父类的某些方法需要声明但又不确定如何实现时,可以先将其声明为抽象方法,那么这个类就是抽象类,用abstract关键字来声明
语法:访问修饰符 abstract 类名{}
- 用abstract关键字来修饰一个方法的时候,该方法就是抽象方法
语法:访问修饰符 abstract 返回类型 方法名(参数列表);//没有方法体
有以下几点需要注意:
- 抽象类不能够被实例化
- 抽象类可以没有abstract方法
- 一旦类被声明为abstract方法,则这个类必须声明为abstract
- abstract只能够修饰类和方法,不能修饰属性和其他的
- 一个类继承了抽象类,则它必须实现抽象类中的所有抽象方法,除非它自己也声明为abstract类
- 抽象方法不能够用private,final和static来修饰,这些关键字都是和重写相违背的
接口
基础
接口就是给出一些没有实现的方法,封装到一起,在根据具体情况把这些方法写出来
语法如下:
1 | interface 接口名{ |
那么什么时候使用接口呢,下面举个例子:现在要制造战斗机,武装直升机,专家只需要将飞机所需的功能/规格定下来即可,剩下的交给别人实现就可以了、
下面来个简单的代码例子:
DBInterface.java
1 | package show; |
MysqlDB.java
1 | package show; |
New.java
1 | import show.MysqlDB; |
有以下几点需要注意的
接口的多态特性
- 多态参数:接口的多态参数指的是在方法定义中,使用接口类型作为参数。由于Java中接口可以被多个类实现,因此在方法中可以传入任何实现了该接口的类的对象,从而实现了多态性
在Java中,父类(或接口)类型的引用可以指向任何子类(或实现类)的对象,并且调用方法时,会根据对象的实际类型执行对应的方法
1 | // 定义一个接口 |
- 多态数组
- 接口存在多态传递现象
1 | public class InterfacePolyPass { |
内部类
基础
一个类内部完整地嵌套另一个类,被嵌套的类称为内部类,嵌套其他类的类称为外部类
内部类的最大特点就是可以直接访问私有属性
1 | class Outer{//外部类 |
举个例子,如下
1 | class Outer { //外部类 |
内部类的分类
定义在外部类局部位置上(比如方法内)
局部内部类
- 定义在外部类的局部位置,比如方法中,并且有类名
- 可以直接访问所有成员,包括私有成员
- 不能添加访问修饰符,因为它其实就是一个局部变量,但可以使用final来修饰
- 仅仅作用于定义它的方法或代码块中
- 外部类访问内部类的成员的话要创建对象来访问
- 外部其他类不能访问局部内部类
- 如果外部类和局部内部类的成员重名时,遵守就近原则,如果想访问外部类的成员的话,可以用
外部类名.this.成员
来访问
1 | public class LocalInnerClass {// |
匿名内部类的使用(重要)
定义在外部类的局部位置,比如方法中,并且没有类名
1 | new 类或接口(参数列表){ |
匿名内部类的特点:
- 没有名字:匿名内部类没有名字,不能在其他地方引用它,只能在声明时使用。
- 通常用于简化代码:特别是当你只需要一个类的一个实例,并且该类不需要被重复使用时,可以用匿名内部类。它通常用来实现接口或者继承类,以提供特定的实现。
- 只能创建一次实例:匿名内部类的创建与实例化是在同一个地方完成的,因此不能在多个地方重复使用该类。
- 语法简洁:匿名内部类允许在代码中定义一个类的同时创建一个该类的实例,省去了为类单独命名和定义的麻烦
例子如下:
1 | public class Main { |
再举一个例子如下:
1 | public class New { |
成员内部类
- 定义在外部类的成员位置上,并且没有static修饰
- 可以直接访问外部类的所有成员,包括私有的
- 可以添加任意访问修饰符,因为它的地位就是一个成员
- 作用域和外部类的其他成员一样,都为整个类体
- 当外部类和内部类重名的时候,内部类访问时遵守就近原则,想访问外部类成员,可以使用
外部类名.this.成员
来访问
1 | public class New { |
静态内部类
- 定义在外部类成员位置,并有static修饰
- 可以直接访问外部类所有的静态成员,包含私有的,但不能直接访问非静态成员
- 当外部类和静态内部类重名时,静态内部类访问时,遵循就近原则,如果想访问外部类的成员,则可以使用
外部类名.成员
来访问
枚举
基础
Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。
例如定义一个颜色的枚举类。
1 | enum Color |
以上枚举类 Color 颜色常量有 RED, GREEN, BLUE,分别表示红色,绿色,蓝色
1 | enum Color |
内部类中使用枚举
枚举类也可以声明在内部类中:
1 | public class Test |
每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。
以上的枚举类 Color 转化在内部类实现:
1 | class Color |
迭代枚举元素
可以使用 for 语句来迭代枚举元素:
1 | enum Color |
在 switch 中使用枚举类
枚举类常应用于 switch 语句中:
1 | enum Color |
values(), ordinal() 和 valueOf() 方法
enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Serializable 和 java.lang.Comparable 两个接口。
values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:
- values() 返回枚举类中所有的值。
- ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
- valueOf()方法返回指定字符串值的枚举常量。
1 | enum Color |
枚举类成员
枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用
枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它
1 | enum Color |
集合
Java 的集合类主要分为两大类:
1.Collection 接口的子类(单列集合):是 Java 集合框架中的根接口,主要用于存储一组元素
常见子接口包括:
List(列表):有序且允许重复的集合,常用实现类包括:
ArrayList
LinkedList
Vector
Set(集合):无序且不允许重复的集合,常用实现类包括:
HashSet
LinkedHashSet
TreeSet
Queue(队列):用于按顺序处理元素,常用实现类包括:
LinkedList
PriorityQueue
2.Map 接口的子类(双列集合):主要用于存储键值对
Collection接口和常用方法
基本操作
Collection
定义了集合对象的基本操作,例如添加、删除、遍历、清空等。这些方法被子类(如 List
、Set
等)继承并具体实现。主要方法包括:
add(E e)
:向集合中添加元素。remove(Object o)
:从集合中移除指定元素。contains(Object o)
:判断集合中是否包含指定元素。size()
:返回集合中元素的个数。isEmpty()
:判断集合是否为空。clear()
:清空集合中的所有元素。
1 | import java.util.ArrayList; |
用 Iterator(迭代器)遍历接口元素
- 主要用于遍历Collection集合中的元素
- 所有实现Collection接口的集合类都有一个Iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
- Iterator仅用于遍历集合,其本身并不存放对象
通常,Iterator
与 while
循环一起使用,通过其方法来进行遍历。
迭代器的工作原理
- 创建迭代器:调用集合对象的
iterator()
方法来创建一个Iterator
实例。这个迭代器指向集合的第一个元素之前的位置。 - 遍历元素:通过
hasNext()
来检查是否有下一个元素,通过next()
来获取下一个元素的值。内部会记录当前位置,每调用一次next()
,迭代器会移动到下一个元素。 - 删除元素(可选):如果需要,可以调用
remove()
方法来删除刚刚通过next()
访问的元素
Iterator
接口包含以下三个核心方法:
boolean hasNext()
: 检查集合中是否还有下一个元素。如果存在下一个元素,则返回true
,否则返回false
。1
2
3if (iterator.hasNext()) {
// Do something
}E next()
: 返回集合中的下一个元素。如果调用时没有元素,通常会抛出NoSuchElementException
。1
E element = iterator.next();
void remove()
: 从底层集合中移除next()
方法返回的上一个元素。这个方法是可选的,如果集合不支持元素的删除操作,可能会抛出UnsupportedOperationException
。
来个简单的例子
1 | import java.util.ArrayList; |
特点
Iterator
提供了一种统一的方式来遍历不同类型的集合。- 它只支持单向遍历,不允许回退。(想再次遍历需要重置迭代器,即
iterator = col.iterator();
) - 与增强的
for
循环相比,Iterator
提供了更细粒度的控制(如删除操作)
增强for循环
增强for循环,可以替代iterator迭代器
特点:其实就是简单版的iterator,本质一样,只能用于遍历集合或者数组
基本语法:
1 | for(元素类型 元素名:集合名或数组名){ |
例子如下:
1 | import java.util.ArrayList; |
List接口和常用方法
基本
- List接口是Collection接口的子接口
- List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
- List集合中的每个元素都有其对应的顺序索引
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
常用方法
以下是 List
接口中的一些常用方法:
void add(E element)
功能: 将指定的元素添加到列表的末尾。
示例
1
2List<String> list = new ArrayList<>();
list.add("Apple");
void add(int index, E element)
功能: 在指定位置插入元素。其后的元素将向右移动。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
list.add(0, "Banana"); // 在索引 0 位置插入元素
E get(int index)
功能: 返回列表中指定索引处的元素。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
String fruit = list.get(0); // 获取索引 0 处的元素
E set(int index, E element)
功能: 用指定的元素替换列表中指定索引位置的元素,并返回被替换的元素。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
list.set(0, "Banana"); // 将索引 0 处的元素替换为 Banana
E remove(int index)
功能: 移除指定索引位置的元素,并返回被移除的元素。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
list.remove(0); // 移除索引 0 处的元素
boolean remove(Object o)
功能: 从列表中移除首次出现的指定元素。如果列表中包含该元素,返回
true
;否则,返回false
。示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
list.remove("Apple"); // 移除元素 "Apple"
int size()
功能: 返回列表中的元素个数。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
int size = list.size(); // 返回列表的大小
boolean isEmpty()
功能: 如果列表不包含元素,则返回
true
;否则,返回false
。示例
1
2List<String> list = new ArrayList<>();
boolean isEmpty = list.isEmpty(); // 检查列表是否为空
boolean contains(Object o)
功能: 如果列表中包含指定的元素,则返回
true
。示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
boolean contains = list.contains("Apple"); // 检查是否包含 "Apple"
int indexOf(Object o)
功能: 返回列表中第一次出现的指定元素的索引,如果列表中不包含该元素,则返回
-1
。示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
int index = list.indexOf("Apple"); // 返回元素 "Apple" 的索引
int lastIndexOf(Object o)
功能: 返回列表中最后一次出现的指定元素的索引,如果列表中不包含该元素,则返回
-1
。示例
1
2
3
4
5List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Apple");
int lastIndex = list.lastIndexOf("Apple"); // 返回 "Apple" 最后出现的索引
void clear()
功能: 移除列表中的所有元素。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
list.clear(); // 清空列表
List<E> subList(int fromIndex, int toIndex)
功能: 返回列表中指定范围内的元素的视图(包含
fromIndex
,不包含toIndex
)。示例
1
2
3
4
5List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
List<String> sublist = list.subList(0, 2); // 返回子列表 ["Apple", "Banana"]
Object[] toArray()
功能: 返回包含列表中所有元素的数组。
示例
1
2
3List<String> list = new ArrayList<>();
list.add("Apple");
Object[] array = list.toArray(); // 转换为数组
boolean addAll(int index, Collection eles)
功能: 从 index 位置开始将 eles 中的所有元素添加进来
示例
1
2
3
4
5
6
7
8List<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 | import java.util.LinkedList; |
LinkedList
vs. ArrayList
:
- ArrayList 更适合随机访问元素,因为它是基于数组实现的,可以通过索引快速访问元素。
- LinkedList 更适合频繁的插入和删除操作,因为它不需要移动大量数据
Set接口和常用方法
基本
无序,即添加和取出的顺序不一致,没有索引
不允许重复元素,所以最多包含一个null
常用方法
Set接口为Collection接口的子接口,所以常用方法和Colletion接口一样
遍历方式
- 可以使用迭代器
- 增强for循环
- 不能使用索引的方式来获取
举个例子如下所示:
1 | import java.util.HashSet; |
Map接口和常用方法
特点
Map
是用于存储键值对(key-value pairs)的集合,每个键唯一对应一个值,即可以通过指定的key找到对应的value不能包含重复的键,但可以包含重复的值。
常见实现包括
HashMap
、TreeMap
和LinkedHashMap
,它们分别有不同的排序和性能特性当有相同的key时,就相当于等价替换:
1
2map.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 | import java.util.HashMap; |
开发中如何选择集合实现类
Collections工具类
Collections是一个操作Set、List和Map等集合的工具类
Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
以下是一些常用的 Collections
方法:
排序:
Collections.sort(List<T> list)
可以对 List 进行升序排序。1
2
3List<Integer> numbers = Arrays.asList(5, 3, 9, 1);
Collections.sort(numbers);
System.out.println(numbers); // 输出 [1, 3, 5, 9]查找最大值和最小值:
Collections.max(Collection<T> coll)
查找集合中的最大值。Collections.min(Collection<T> coll)
查找集合中的最小值。
1
2int max = Collections.max(numbers); // 返回 9
int min = Collections.min(numbers); // 返回 1反转:
Collections.reverse(List<?> list)
可以将 List 反转。1
2Collections.reverse(numbers);
System.out.println(numbers); // 输出 [9, 5, 3, 1]随机打乱:
Collections.shuffle(List<?> list)
可以将 List 随机打乱顺序。1
2Collections.shuffle(numbers);
System.out.println(numbers); // 输出打乱后的顺序不可修改集合:
Collections.unmodifiableList(List<? extends T> list)
可以返回一个不可修改的集合。1
2List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
// 试图修改 unmodifiableList 会抛出 UnsupportedOperationException
泛型
基础
Java中的泛型(Generics)是Java 5引入的一项重要特性,允许类、接口和方法能够操作指定类型的对象,而无需在编写代码时明确指定类型。这种机制通过将类型参数化,提供了更高的代码复用性和类型安全性
传统方法面临的问题
不能对加入到集合中的数据类型进行约束(比如要求输入的是Dog类,结果不小心输入了Cat类)
遍历的时候,需要进行类型转换,影响效率
1
2
3
4
5for (Object o : arrayList) {
//向下转型 Object ->Dog
Dog dog = (Dog) o;
System.out.println(dog.getName() + "-" + dog.getAge());
}
对于泛型举个例子如下:
当我们
ArrayList<Dog>
表示存放到 ArrayList 集合中的元素是 Dog 类型 (细节后面说…)如果编译器发现添加的类型,不满足要求,就会报错
在遍历的时候,可以直接取出 Dog 类型而不是 Object
1
2
3for (Dog dog : arrayList) {
System.out.println(dog.getName() + "-" + dog.getAge());
}public class ArrayList<E> {}
E 称为泛型,那么 Dog->E
泛型的优点:
- 编译时检查了添加元素的类型,提高了安全性
- 减少了类型转换的次数,提高了效率
- 不会提示编译警告
利用
1. 泛型类
定义一个泛型类 Box
,用来存储任意类型的对象:
1 | public class Box<T> { |
使用泛型类的例子:
1 | public class Main { |
输出:
1 | String: Hello |
2. 泛型方法
定义一个泛型方法 printArray
,可以打印任何类型数组的内容:
1 | public class Main { |
输出:
1 | 1 |
3. 泛型接口
定义一个泛型接口 Container
,用来存储和获取任意类型的对象:
1 | public interface Container<T> { |
使用泛型接口的例子:
1 | public class Main { |
输出:
1 | Hello Generics |
4. 泛型通配符
使用通配符 <? extends T>
和 <? super T>
来限定类型的范围:
上限通配符 <? extends T>
表示类型必须是T
或其子类。定义一个方法打印所有类型为Number
及其子类的列表。
1 | import java.util.List; |
输出:
1 | 1 |
下限通配符 <? super T>
表示类型必须是T
或其父类。定义一个方法将元素添加到列表中,列表可以是Integer
或其父类的类型。
1 | import java.util.List; |
输出:
1 | 1 |
总结
- 泛型类:可以让类存储和操作多种不同类型的数据。
- 泛型方法:允许方法适应多种类型。
- 泛型接口:使接口可以与不同类型的实现类进行交互。
- 通配符:提供了更灵活的类型限定。
常见泛型符号:
T
:Type,表示类型。E
:Element,常用于集合类中。K
:Key,常用于键值对。V
:Value,常用于键值对。
注意事项:
- T、E只能是引用类型
- 在给泛型指定具体类型后,可以传入该类型或者其子类类型
- 当写
List list3 = new ArrayList();
,默认它的泛型为E,也就是Object
自定义泛型
自定义泛型类
基本语法
1 | class 类名 <T,R...>//...表示可以有多个泛型 |
注意事项:
- 普通成员可以使用泛型
- 使用泛型的数组,不能初始化
- 静态方法中不能使用类的泛型
- 泛型类的类型,是在创建对象的时候确定的(因为创建对象时,需要指定类型)
- 如果在创建对象的时候没有指定类型,默认为Object
下面举个相关的例子:
1 | import java.util.Arrays; |
自定义泛型接口
基本语法:
1 | interface 接口名<T,R>{ |
注意事项:
- 接口中,静态成员不能使用泛型
- 泛型接口的类型,在继承接口或者实现接口的时候确定
- 没有指定类型,默认为Object
举例如下:
1 | //在继承接口 指定泛型接口的类型 |
自定义泛型方法
基本语法:
1 | 修饰符 <T,R>返回类型 方法名(参数列表){ |
注意事项:
- 泛型方法可以定义在普通类中,也可以定义在泛型类中
- 当泛型方法被调用时,类型就会确定
- public void eat(E e){},修饰符后没有<T,R…>eat方法不是泛型方法,而是使用了泛型
举个例子如下
1 | import java.util.ArrayList; |