Java核心技术卷梳理解读-String类与面向对象程序设计基础
鉴于Java核心技术卷中的内容比较繁复,故进行关键内容整理解读
细谈String类
String类的实现方式
String类用于保存字符串,且是一个final
类型,并且在Java中使用Unicode编码,在它的类视图中实现了以下接口:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
...
}
java.io.Serializable
:是一个标记接口(marker interface),用于标记类的实例可以被序列化。序列化是指将对象转换为字节序列,可以将其存储到文件中或通过网络传输。实现Serializable
接口的类可以通过 Java 序列化机制实现对象的持久存储和传输。Comparable<String>
:是一个泛型接口,表示实现类可以与指定类型进行比较。Comparable
接口中有一个compareTo
方法,用于比较实现类的对象与另一个对象的顺序。CharSequence
:是一个接口,代表一个字符序列,其中的字符可以通过索引访问。CharSequence
接口的实现类可以是String
、StringBuilder
、StringBuffer
等。Constable
:是一个接口,表示实现类的对象是一个常量。这个接口通常用于描述可以在编译时被识别为常量的对象。ConstantDesc
:是一个类,用于描述java.lang.constant.ConstantDesc
类型的常量描述符。这个类通常用于支持 Java 语言规范中的常量描述符。
需要注意的是,在jdk8中String实现的接口要少一些:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
}
在JDK9之后,
String
的底层实现由char数组改成了byte
数组,现在比较流行的说法是在存储ASCII字符时,可以有更少的内存占用。原本的char
类型占用两个字节,而byte
只占用一个字节。并且还引入了coder
字段作为区分的标志,使得占用两字节的Unicode字符仍然实现为char
类型数组。还需要注意的是final类型的数组实质上是可以修改其内部的值的,例如:
public class StringMain { public static void main(String[] args) { final char[] str1 = {'a', 'b', 'c'}; str1[1] = 'd'; } }
而
String
类型的不可变体现在,String
类型本质上并没有实现可以修改内部维护数组方法,并且其因为是final
类型所以不能够被继承,这两点共同促成了其不可变的特性,并且String
类型并未实现[]
直接索引的方式
String对象的构建
public class StringTestMain {
public static void main(String[] args) {
char[] strArray = {'a', 'b', 'c'};
String str1 = "233";
String str2 = new String();
String str3 = new String(str1);
String str4 = new String(strArray);
String str5 = new String(strArray, 1, 1);
}
}
char[] strArray = {'a', 'b', 'c'};
:这是一个字符数组的定义和初始化,包含了字符'a'
、'b'
和'c'
。String str1 = "233";
:这是一个字符串的直接赋值方式,将"233"
赋给str1
。String str2 = new String();
:这是使用无参构造方法创建了一个空的字符串对象str2
。String str3 = new String(str1);
:这是使用一个已有的字符串str1
创建了一个新的字符串对象str3
。String str4 = new String(strArray);
:这是使用字符数组strArray
创建了一个新的字符串对象str4
,它会将字符数组中的所有字符连接起来。String str5 = new String(strArray, 1, 1);
:这是使用字符数组strArray
的一部分(从索引 1 开始,长度为 1)创建了一个新的字符串对象str5
,所以str5
中的字符是'b'
。
不同构建方式差异
注意以下两者的区别:
String str1 = "2333";
String str2 = new String("2333");
- 第一种方式创建字符串时,先在常量池中查看是否存在该字符串,如果存在则直接指向,否则将进行创建,最终
str1
引用指向的是常量池中的地址 - 第二种创建方式创建时,会先在堆中创建空间,然后通过其维护的value属性来管理,先在常量池中查看该字符串是否存在,存在则value直接指向,否则进行创建,最终
str2
引用的仍然是堆的地址
public class StringTestMain {
public static void main(String[] args) {
String str1 = "2333";
String str2 = new String("2333");
String str3 = "2333";
System.out.println(str1==str3); // 输出true
System.out.println(str1==str2); // 输出false
System.out.println(str1.equals(str3)); // 输出true
System.out.println(str1.equals(str2)); // 输出true
}
}
注意:对于构建之时还有一点极为重要:
String str = "Hello" + "Java";
上述语句在构建之时将会进行优化,只会在常量池中新建字符串
HelloJava
,不会说新建三个字符串(不过这点实质上还是取决于JVM的实现)
两字符串对象构建jdk8
相较于String str = "Hello" + "Java";
,这里有一点必须要注意,最后的str3取的是堆空间中的引用:
public class TestMain {
public static void main(String[] args) {
String str1 = "123";
String str2 = "456";
String str3 = str1 + str2;
}
}
我们在idea中进行Debug,使用强制步入可以进入官方库中的方法,在运行到目标行时,我们发现它先构造了一个StringBuilder
类对象,其默认容量是16个字符:
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() {
super(16);
}
然后构建调用了append
方法,先加入的是str1
,然后才是str2
,最后将构建好的StringBuilder
转换为String对象:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
两字符串对象构建jdk17
jdk17在字符串实现方式与jdk8不一样后,在处理拼接时也出现了不一样的情况,jdk定义了一个拼接方法来处理两个字符串并且进行了编码方式判断(coder字段)
static String simpleConcat(Object first, Object second) {
String s1 = stringOf(first);
String s2 = stringOf(second);
if (s1.isEmpty()) {
// newly created string required, see JLS 15.18.1
return new String(s2);
}
if (s2.isEmpty()) {
// newly created string required, see JLS 15.18.1
return new String(s1);
}
// start "mixing" in length and coder or arguments, order is not
// important
long indexCoder = mix(initialCoder(), s1);
indexCoder = mix(indexCoder, s2);
byte[] buf = newArray(indexCoder);
// prepend each argument in reverse order, since we prepending
// from the end of the byte array
indexCoder = prepend(indexCoder, buf, s2);
indexCoder = prepend(indexCoder, buf, s1);
return newString(buf, indexCoder);
}
然后在newString时又进行了检测编码问题:
static String newString(byte[] buf, long indexCoder) {
// Use the private, non-copying constructor (unsafe!)
if (indexCoder == LATIN1) {
return new String(buf, String.LATIN1);
} else if (indexCoder == UTF16) {
return new String(buf, String.UTF16);
} else {
throw new InternalError("Storage is not completely initialized, " +
(int)indexCoder + " bytes left");
}
}
intern方法
intern()
方法会尝试将字符串对象转换为常量池中实际字符串,如果常量池中已经存在相同内容的字符串,则返回池中的引用,否则将该字符串添加到常量池中并返回引用。
public class StringTestMain {
public static void main(String[] args) {
String str1 = "2333";
String str2 = new String("2333");
System.out.println(str1 == str2.intern()); // 输出true
}
}
常用方法
方法 | 描述 |
---|---|
equals(String) | 区分大小写,判断内容是否与给定字符串相等。返回true如果内容相等,否则返回false。 |
equalsIgnoreCase(String) | 忽略大小写,判断内容是否与给定字符串相等。返回true如果内容相等,否则返回false。 |
length() | 返回字符串的长度,即包含的字符个数。 |
indexOf(char) | 返回指定字符在字符串中第一次出现的索引。索引从0开始,如果未找到字符,则返回-1。 |
lastIndexOf(char) | 返回指定字符在字符串中最后一次出现的索引。索引从0开始,如果未找到字符,则返回-1。 |
substring(int, int) | 返回一个新的字符串,包含原始字符串中从startIndex 到endIndex 之间的字符,包括startIndex 的字符。如果endIndex 超出字符串长度,则截取到字符串末尾。如果startIndex 大于endIndex ,则返回空字符串。 |
trim() | 返回一个新的字符串,去除原始字符串首尾的空格。 |
charAt(int) | 返回指定索引处的字符。注意,索引从0开始,如果索引超出字符串长度或为负数,则抛出IndexOutOfBoundsException 异常。 |
toUpperCase() | 将字符串中的所有字符转换为大写。 |
toLowerCase() | 将字符串中的所有字符转换为小写。 |
concat(String) | 将指定字符串连接到此字符串的末尾。 |
replace(char, char) | 替换字符串中的字符。将原字符串中所有出现的指定字符替换为新字符。 |
split(String) | 分割字符串。根据给定的正则表达式将字符串拆分为子字符串数组。需要注意,某些分割字符需要转义,如/ 、\\ 等。 |
compareTo(String) | 比较两个字符串的大小。返回值为0表示两个字符串相等,负数表示当前字符串小于参数字符串,正数表示当前字符串大于参数字符串。 |
toCharArray() | 将字符串转换为字符数组。返回一个新的字符数组,包含字符串中的所有字符。 |
format(String, ...) | 格式化字符串。使用指定的格式字符串和参数将字符串格式化为新的字符串。格式字符串中的特殊格式符号如下:%s 表示字符串,%c 表示字符,%d 表示整型,%.2f 表示 |
StringBuffer类
StringBuffer
类在 Java 中用于表示可变的字符串。与 String
类不同的是,StringBuffer
的内容可以修改,而 String
的内容是不可变的。StringBuffer
类提供了许多方法来操作字符串,它是一个容器,支持例如追加、插入、删除和替换等操作。
StringBuffer
是线程安全的,可以在多线程环境中安全地使用。然而,由于线程安全的实现可能会带来一些性能损失,因此在不需要线程安全保证的情况下,可以使用StringBuilder
类来代替StringBuffer
,StringBuilder
类提供了与StringBuffer
类相同的 API,但不保证线程安全。
StringBuffer类的实现
StringBuffer类继承自抽象类AbstractStringBuilder,也实现了序列化的接口,和索引访问的接口,由于不需要频繁修改对象地址信息,所以它相较于String要高效一些
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuffer() {
super(16);
}
...
}
值得一提的是StringBuffer可以看作是一个加强版的String,并且它的许多属性都是和String一致的,但是作为一个容器类型,它默认的value属性是存储在堆区的,并且在默认情况下其初始化的长度是16,如果说接收的字符串长度大于了16,它会在已有字符串的长度基础上增加16:
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
在进行append方法时,超出长度时将会增加其抽象父类的动态长度值count属性,如以下代码中的截取部分:
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
StringBuffer构造器
- 初始化长度为16的字符串缓冲区
public StringBuffer() {
super(16);
}
- 初始化指定长度的字符串缓冲区
public StringBuffer(int capacity) {
super(capacity);
}
- 以指定字符串初始化字符串缓冲区,基础长度上增加16
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
- 和上一个构造器类似,只不过是直接转换的
CharSequence
指向的实现了此接口的对象
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
以下是一些常用的 StringBuffer
方法:
append(String str)
:将指定的字符串追加到此字符序列。insert(int offset, String str)
:将指定的字符串插入此字符序列中的指定位置,其插入位置是序列的第九个位置,其后的内容自动后移。delete(int start, int end)
:删除此字符序列中指定位置的子字符串,区间是数组索引计算左闭右开。replace(int start, int end, String str)
:使用指定的字符串替换此字符序列中指定位置的子字符串,同样左闭右开。reverse()
:将此字符序列用其反转形式取代。indexOf(String str)
:查找指定字串出现位置的第一个索引,遵循数组索引
StringBuilder类
StringBuilder
类是 Java 中用于处理字符串的一个类,它允许我们在一个可变的字符串对象中进行操作,而不是像 String
类那样创建新的字符串对象。这使得 StringBuilder
在需要频繁修改字符串的情况下更加高效,相比于StringBuffer它在单线程实现上也要快上一些。它的构造也是继承自AbstractStringBuilder,并且使用方式也与StringBuffer非常类似:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
...
}
主要特点和用法包括:
-
可变性(Mutable):
StringBuilder
对象的内容可以被改变,可以进行插入、追加、删除等操作。 -
性能优化:
StringBuilder
被设计为在对字符串进行频繁修改时能够提供更好的性能。因为String
对象是不可变的,每次对String
进行修改都会创建一个新的String
对象,而StringBuilder
允许直接在原有的对象上进行修改,避免了频繁创建对象的开销。 -
线程不安全:
StringBuilder
不是线程安全的,如果需要在多线程环境下使用可变的字符串,应该使用StringBuffer
类。
使用 StringBuilder
类示例:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" world"); // 追加字符串
sb.insert(5, ","); // 在指定位置插入字符
sb.delete(5, 6); // 删除指定范围的字符
sb.reverse(); // 反转字符串
String result = sb.toString(); // 转换为不可变的字符串
System.out.println(result);
字符串家族三者比较
String
、StringBuffer
和 StringBuilder
是 Java 中用于处理字符串的三个类,它们各有特点和适用场景。
-
String(不可变字符串):
- 特性:
String
是不可变的,即一旦创建就不能修改它的值。任何对String
的操作都会返回一个新的String
对象。 - 优点:线程安全,可以在多线程环境下共享,因为不可修改的特性保证了安全性。
- 缺点:每次操作都会创建一个新的对象,可能会导致性能问题,特别是在频繁操作字符串时。
- 特性:
-
StringBuffer(可变字符串,线程安全):
- 特性:
StringBuffer
是可变的,可以对其进行插入、追加、删除等操作。它是线程安全的,适合在多线程环境中使用。 - 优点:线程安全,适合在多线程环境中使用。操作不会创建新的对象,因此在频繁操作字符串时性能较好。
- 缺点:相比于
StringBuilder
,StringBuffer
的性能稍差,因为它的方法是同步的,会有额外的同步开销。
- 特性:
-
StringBuilder(可变字符串,非线程安全):
- 特性:
StringBuilder
也是可变的,可以对其进行插入、追加、删除等操作。与StringBuffer
不同的是,StringBuilder
不是线程安全的。 - 优点:性能比
StringBuffer
更好,因为不需要处理同步的开销。 - 缺点:不适合在多线程环境中使用,如果在多线程环境中使用
StringBuilder
,需要自行处理线程安全问题
- 特性:
面向对象-类的访问修饰符
public(公共类)
public: 表示该类是公共的,可以在任何地方被访问。我们使用以下类来进行包外访问
package power_control;
public class ExtentOfPower {
public int one = 1;
public int two = 2;
public int three = 3;
public void getAllValue(){
System.out.println(one + " " + two + " " + three);
}
}
在此包中进行访问:
package out_vist_class;
import power_control.ExtentOfPower;
public class OutVisitTest {
public static void main(String[] args) {
ExtentOfPower obj = new ExtentOfPower();
obj.getAllValue();
}
}
default(包私有)
default(包私有): 如果不使用任何修饰符,默认为包私有。只能在同一包中被访问,将上述的类中的public
去掉,我们再回到访问包中,IDEA将会提示:
>
JavaTest: 构建 失败 在2023/12/11 13:45,3个错误
OutVisitTest.java src\out_vist_class 3个错误
power_control.ExtentOfPower在power_control中不是公共的;无法从外部程序包中对其进行访问:2
power_control.ExtentOfPower在power_control中不是公共的;无法从外部程序包中对其进行访问:6
power control.ExtentOfPower在power_control中不是公共的;无法从外部程序包中对其进行访问:6
1秒325毫秒
注意:类的访问修饰没有保护(protected)这一说法
面向对象-类字段访问修饰符
public(公共的)
- 修饰的成员对所有类可见,没有访问限制。
- 在当前包和其他包中都可以访问。
示例类的内容:
package power_control;
public class ExtentOfPower {
public int one = 1;
}
包外访问:
package out_vist_class;
import power_control.ExtentOfPower;
public class OutVisitTest {
public static void main(String[] args) {
ExtentOfPower obj = new ExtentOfPower();
System.out.println(obj.one);
}
}
private(私有的)
- 修饰的成员只能在声明它的类中访问,对外部类不可见。外部包自然也不可访问
- 通常用于封装类的实现细节,实现信息隐藏
class ExtentOfPower {
private int one = 1;
public int two = 2;
}
public class MainOfExtentPower{
public static void main(String[] args){
ExtentOfPower obj = new ExtentOfPower();
System.out.println(obj.one +" " + obj.two);
}
}
此处IDEA将提示:java: one 在 power_control.ExtentOfPower 中是 private 访问控制
protected(受保护)
- 修饰的成员对同一包内的类和所有子类可见,但对其他包的类不可见。
- 用于继承关系中,允许子类访问父类的受保护成员
同包中的子类与其他类
class ExtentOfPower {
private int one = 1;
public int two = 2;
protected int three = 3;
}
class ExtentFromExtentPower extends ExtentOfPower{
}
public class MainOfExtentPower{
public static void main(String[] args){
ExtentFromExtentPower obj = new ExtentFromExtentPower();
System.out.println(obj.two + " " + obj.three);
var object = new ExtentOfPower();
System.out.println(object.three);
}
}
其他包中的非子类:
package power_control;
public class ExtentOfPower {
protected int one = 1;
}
package out_vist_class;
import power_control.ExtentOfPower;
public class OutVisitTest {
public static void main(String[] args) {
ExtentOfPower obj = new ExtentOfPower();
System.out.println(obj.one);
}
}
IDEA提示:java: one 在 power_control.ExtentOfPower 中是 protected 访问控制
default(包级私有)
- 修饰的成员只对同一包内的类可见,对其他包的类不可见。
- 如果没有指定访问控制符,则默认为包级私有。
package power_control;
public class ExtentOfPower {
int one = 1;
}
去掉访问控制符后,在外部包中类进行访问,IDEA提示:java: one在power_control.ExtentOfPower中不是公共的; 无法从外部程序包中对其进行访问
类的继承-关键字extends
class SuperClass{
protected int id;
public SuperClass(int aId){
id = aId;
}
public void showAllInfo(){
System.out.println(id);
}
}
父类书写,没什么特殊的,注意访问控制的关键字就行,重点在于子类:
class SubClass extends SuperClass {
private String name;
public SubClass(int aId, String aName){
super(aId);
name = aName;
}
@Override
public void showAllInfo() {
super.showAllInfo();
System.out.println(name);
}
}
注解如下:
- 在父类构造器具有参数时,我们需要使用super关键字来调用父类的构造器,并且提供足够数量的正确类型关键字
- 在在子类中我们想要调用父类中已进行重写的方法,我们可以使用关键字
super
来指定父类的重名方法 - 对于重写的方法我们通常会使用
@Override
注解 - 注意Java中没有多重继承这一概念,但是有类似功能,如接口(interface)
- 其他方面与C++基础语法层面无太大不同,且继承也是
is-a
关系
@Override
是 Java 编程语言中的一个注解(Annotation)。它的作用是告诉编译器,被注解的方法是一个重写(override)父类或接口中的方法。在 Java 中,当你声明一个类继承自另一个类,或者实现一个接口时,通常你可以重写父类或接口中的方法。在这种情况下,如果你打算重写一个方法,使用
@Override
注解可以帮助你确保你的方法真的是在父类或接口中存在的,而不是一个新的方法。使用
@Override
的好处有:
编译时检查: 编译器会检查被注解的方法是否真的重写了父类或接口中的方法。如果没有正确重写,编译器将给出错误提示。
@Override public void myMethod() { // Your implementation }
文档说明:
@Override
注解也提供了一种文档方式,让其他程序员清楚地知道这个方法是重写的。/** * This method overrides the corresponding method in the superclass. */ @Override public void myMethod() { // Your implementation }
阻止继承final
在 Java 中,final
是一个关键字,可以用于修饰类、方法和变量。当 final
用于修饰一个类时,它阻止这个类被继承。一旦一个类被声明为 final
,它不能被其他类继承。
下面是一个简单的例子:
final class FinalClass {
// 类的内容
}
// 编译错误,无法继承 final 类
// class SubClass extends FinalClass {}
在这个例子中,FinalClass
被声明为 final
类,因此任何尝试继承它的操作都会导致编译错误。使用 final
类的主要目的是为了防止类的进一步修改和继承,以确保该类的行为和实现不会被子类修改。
注意:final类型的方法也不能被继承
super关键字
在Java中,super
是一个关键字,用于引用父类的成员(字段或方法),以及调用父类的构造方法。super
的主要用途是在子类中访问和调用从父类继承而来的成员,即访问父类的非private
属性与方法:
-
调用父类的构造方法: 在子类的构造方法中,可以使用
super
关键字调用父类的构造方法。这通常用于在子类对象初始化时执行父类的初始化操作。super()
必须是构造方法的第一句语句。public class ChildClass extends ParentClass { public ChildClass() { super(); // 调用父类的无参构造方法 // 子类的其他初始化操作 } }
如果父类有参数的构造方法,也可以使用
super
调用相应的构造方法:public class ChildClass extends ParentClass { public ChildClass(int value) { super(value); // 调用父类的带参数构造方法 // 子类的其他初始化操作 } }
-
访问父类的成员: 在子类中,可以使用
super
关键字来访问父类中的字段或方法。这在子类和父类具有相同成员名时很有用。public class ChildClass extends ParentClass { private int childField; public ChildClass(int value) { super(value); // 调用父类的构造方法 this.childField = value; } public void display() { super.display(); // 调用父类的方法 System.out.println("Child Field: " + this.childField); } }
这里super.display()
调用了父类的 display
方法,而不是子类中可能存在的同名方法。
所有类的超类Object
在Java中,所有类都直接或间接地继承自Object
类。Object
类是Java类层次结构的根类,它包含了所有对象共有的方法。以下是Object
类的一些常用方法:
toString()
方法: 该方法返回对象的字符串表示形式。默认的实现返回的是类的名称,后跟 “@” 和对象的哈希码值。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
通常,为了更好地表示对象,你可以在自定义类中重写toString()
方法。
equals(Object obj)
方法: 用于比较两个对象是否相等。默认的实现是比较对象的引用是否相等,即是否指向同一内存地址。可以根据实际需要覆盖该方法。
public boolean equals(Object obj) {
return (this == obj);
}
hashCode()
方法: 返回对象的哈希码值。默认的实现是返回对象的内存地址的哈希码。
public int hashCode() {
return super.hashCode();
}
getClass()
方法: 返回对象的运行时类。
public final Class<?> getClass()
clone()
方法: 用于创建并返回对象的副本。要使用clone()
方法,类必须实现Cloneable
接口,否则会抛出CloneNotSupportedException
异常。
protected Object clone() throws CloneNotSupportedException
finalize()
方法: 在垃圾回收器将对象从内存中清除之前,该方法被调用。可以重写该方法进行资源释放等清理操作。
protected void finalize() throws Throwable
这些是Object
类的主要方法。注意事项包括:
-
equals()和hashCode()一致性: 如果两个对象通过equals()方法相等,那么它们的hashCode()值应该相等。这是为了保证在使用散列表等数据结构时能够正确地工作。
-
谨慎使用clone()方法:
clone()
方法会创建一个对象的副本,但可能会导致意想不到的结果。最好是实现Cloneable
接口并重写clone()
方法,确保正确处理对象的复制。 -
toString()可读性: 重写
toString()
方法时,应该返回一个清晰、简洁且可读性强的字符串,以便在日志、调试和其他情况下更容易理解对象的状态。
重写equals方法
Object的equals的实现
- 在object的equals方法实现非常简单,它默认判断的是地址是否相同以此来判断对象是否是同一个
public boolean equals(Object obj) {
return (this == obj);
}
String的equals的实现
jdk9及之后的String实现
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
上述的代码来自于jdk17的实现,由于该版本的字符串实现改为了bytes数组,所以不太直观:
- 首先,通过
this == anObject
判断两个对象是否是同一个实例,如果是,则直接返回true
,表示相等。 - 然后,通过
anObject instanceof String aString
判断传入的对象是否是 String 类型的实例,如果不是,则返回false
。 - 接着,如果启用了紧凑字符串(
COMPACT_STRINGS
),则通过this.coder == aString.coder
判断两个字符串的编码方式是否相同。 - 最后,调用
StringLatin1.equals(value, aString.value)
方法比较两个字符串的值是否相等,其中value
是当前字符串对象的值,aString.value
是传入字符串对象的值。
在 Java 中,字符串通常是以 UTF-16 编码存储的,每个字符占用 2 个字节的内存空间。然而,在某些情况下,这种存储方式可能会浪费空间,特别是当字符串中的字符都是 ASCII 字符(即 Unicode 编码在 0 到 127 之间的字符)时,每个字符只需要占用 1 个字节的空间。
为了解决这个问题,从 JDK 9 开始,Java 引入了紧凑字符串(Compact Strings)的概念。在紧凑字符串模式下,Java 使用一种称为 Latin1 的编码方式来存储只包含 ASCII 字符的字符串,这样就可以节省一半的内存空间。
StringLatin1
是 JDK 中的一个包私有类,用于处理 Latin1 编码的字符串操作。Latin1 编码是一种单字节编码,可以表示 ISO-8859-1 字符集中的字符,即 Unicode 编码范围在 0 到 255 之间的字符。在 JDK 的实现中,
StringLatin1
类提供了一些静态方法来操作 Latin1 编码的字符串,比如equals
方法用于比较两个 Latin1 编码的字符串是否相等。这些方法通常用于优化处理只包含 ASCII 字符的字符串,因为在这种情况下,Latin1 编码可以节省内存空间。需要注意的是,
StringLatin1
类是 JDK 内部使用的类,不是公共 API 的一部分,因此不建议直接在应用程序中使用它。
jdk8的String实现
相较于上述复杂的实现,jdk8的实现就要简单许多,它会直接依次比较char数组中的每一个值:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
equals重写案例
class TestObject {
public int id;
public String name;
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (other instanceof TestObject) {
TestObject other1 = (TestObject) other;
if (this.id == other1.id && this.name.equals(other1.name))
return true;
}
return false;
}
}
需要注意的是,为了防止潜在的错误(不同类型对象比较也会报错),我们需要对类型进行判断才进行强制转换,并且对于一些作为其内部属性的对象,我们也应该调用其对应的equals
方法
重写hashCode方法
hashCode()
方法是 Java 中 Object 类的一个方法,用于返回对象的哈希码值。哈希码值是一个整数,通常用于确定对象在哈希表中的存储位置。在 Java 中,哈希表主要由 HashMap、HashSet 等集合类使用。
在默认情况下,hashCode()
方法返回对象的内存地址经过哈希算法计算得到的值(不等同于地址)。但是,根据需要,可以在自定义类中重写 hashCode()
方法,以便根据对象的内容生成哈希码。在重写 hashCode()
方法时,通常需要遵循以下规则:
- 如果两个对象根据
equals()
方法比较相等,那么它们的hashCode()
方法应该返回相同的值。 - 如果两个对象根据
equals()
方法比较不相等,不要求它们的hashCode()
方法返回不同的值,但为了提高哈希表的性能,通常希望它们的hashCode()
方法返回不同的值。
public class MainTest {
public static void main(String[] args) {
String str1 = "233";
String str2 = "233";
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());
}
}
public int hashCode() {
return Objects.hash(name, age);
}
重写toString方法
toString()
方法是 Java 中 Object 类的一个方法,用于返回对象的字符串表示。在默认情况下,toString()
方法返回一个由类名和对象内存地址组成的字符串,格式为 ClassName@HashCode
。但是,通常需要在自定义类中重写 toString()
方法,以便返回对象的有意义的字符串表示,以便于调试和日志记录。
// Object的toString
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在 Java 中,有一些情况下会自动调用 toString()
方法:
- 当使用
System.out.println()
方法打印对象时,会自动调用对象的toString()
方法,将返回的字符串输出到控制台。 - 在使用字符串连接操作符
+
连接一个对象时,会自动调用对象的toString()
方法,将返回的字符串与其他字符串连接起来。 - 在调用
String.valueOf()
方法将一个对象转换为字符串时,会自动调用对象的toString()
方法。 - 在使用一些日志库或调试器输出对象信息时,也会自动调用对象的
toString()
方法。
重写 toString()
方法时,通常会根据对象的属性生成一个包含对象状态的字符串。例如,如果有一个名为 Person
的类,包含 name
和 age
两个属性,那么可以重写 toString()
方法如下:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
在上面的示例中,重写的 toString()
方法返回一个包含 name
和 age
属性的字符串,格式为 Person{name='John', age=30}
。这样的字符串表示更容易理解和使用。使用重写的 toString()
方法可以提供更好的可读性和可用性,特别是在调试代码和记录日志时。
finalize方法
finalize()
方法是 Java 中 Object 类的一个方法,用于在垃圾收集器将对象从内存中回收之前执行一些清理操作。不过对于Java来说这个方法使用也并不多见。从 Java 9 开始,finalize()
方法已被标记为弃用,因为它存在一些问题,如无法保证何时会被调用,以及可能导致不可预测的行为和性能问题。
在旧版本的 Java 中,finalize()
方法的调用时机是在对象被垃圾收集器回收之前。具体来说,当垃圾收集器发现一个对象没有被任何引用指向(即对象变为不可达时),会将其标记为“可回收”状态。在将这些可回收对象释放内存之前,垃圾收集器会调用这些对象的 finalize()
方法,以便对象在被销毁之前执行一些清理操作。
然而,需要注意的是,finalize()
方法的调用时机是不确定的,不能保证在对象变为不可达后立即调用。因此,不能依赖于 finalize()
方法来执行关键的清理操作,因为这可能会导致资源泄漏或不可预测的行为。常见的 finalize()
方法操作包括:
-
关闭未关闭的资源,如文件、流等。
-
取消注册对象,如取消注册监听器或观察者。
-
清理临时文件或目录。
-
打印日志或记录对象销毁的信息。
在 Java 9 中,推荐使用
java.lang.ref.Cleaner
类来代替finalize()
方法,以执行对象的清理操作。Cleaner
类提供了更灵活和可靠的方式来执行清理操作,避免了finalize()
方法的一些问题。如果仍然需要在旧代码中使用
finalize()
方法,应该谨慎处理,避免依赖于它来执行关键的清理操作。理想情况下,应该通过显式的关闭资源或实现AutoCloseable
接口来确保资源的释放和清理。
多态实现与使用
多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用一个抽象的类型来表示多个具体的类型,并且在运行时可以动态地选择使用哪个具体类型。多态有两种主要形式:编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态(方法重载)
class SuperClass{
protected int id;
public SuperClass(int aId){
id = aId;
}
public void showAllInfo(){
System.out.println(id);
}
public void showAllInfo(String greeting){
System.out.println(greeting + String.valueOf(id));
}
}
如上所示,使用接收不同的参数的方法来重载方法,其调用不同方法提供不同参数时完成多态,定义对象后就不再改变
运行时多态(继承重写)
编译对象可以与引用对象不一致,在运行时决定其调用的方法
我们回到继承中书写的两个类,我们在main方法中书写以下内容:
public class SubClassMain {
public static void main(String[] args){
SuperClass object = new SuperClass(233);
object.showAllInfo();
object = new SubClass(2333, "WildPointer");
object.showAllInfo();
}
}
在main
方法运行时,我们编译时建立了一个超类的变量,让超类变量引用子类或超类对象,程序开始运行时,会依据我们实际引用的对象来调用我们对应的方法,从而实现多态
向上转型与向下转型
考虑以下继承状态:
class SuperClass{
protected int id;
public SuperClass(int aId){
id = aId;
}
public void showAllInfo(){
System.out.println(id);
}
}
class SubClass extends SuperClass {
private String name;
public SubClass(int aId, String aName){
super(aId);
name = aName;
}
@Override
public void showAllInfo() { //
super.showAllInfo();
System.out.println(name);
}
public void greetingSubClassObject(String greeting){
System.out.println(greeting + String.valueOf(id) + name);
}
}
向上转型(父类变量引用子类对象)
public class SubClassMain {
public static void main(String[] args){
SuperClass object = new SubClass(233, "WildPointer");
object.showAllInfo();
object.greetingSubClassObject("Hello");
}
}
运行上述代码,我们会收到致命错误一个:
D:\CodeProjects\IntelljIDEAprojects\JavaTest\src\javaclass\SubClassMain.java:37:15
java: 找不到符号
符号: 方法 greetingSubClassObject(java.lang.String)
位置: 类型为javaclass.SuperClass的变量 object
原因是父类引用无法调用子类的特有方法,只能够根据访问控制来调用父类中存在的方法
向下转型(父类引用转为子类引用)
向下转型是将一个父类类型的引用变量转换为其子类类型的引用变量的过程。这种类型的转换在 Java 中需要显式地进行,因为在编译时无法确定对象的真实类型。在进行向下转型之前,通常需要使用
instanceof
运算符来检查对象的类型,以避免类型转换异常(ClassCastException
)。
- 语法结构
子类名 子类变量名 = (子类名)父类引用变量名
向下转型的注意事项:只能强制转换父类的引用,不能强转父类的对象,并且要求父类的引用必须指向的是当前目标类型的对象,即父类引用的对象必须是该子类的对象,在向上转型中的问题,我们便可以使用向下转型来解决:
if(object instanceof SubClass){
SubClass temp = (SubClass) object;
temp.greetingSubClassObject("Hello");
}
instanceof
是 Java 中的一个关键字,用于测试对象是否是特定类的实例,或者是否实现了特定的接口。它的语法如下:object instanceof Class/Interface
其中,
object
是要测试的对象,而Class/Interface
是要测试的类或接口。使用instanceof
可以进行类型检查,以便在运行时确定对象的类型,从而采取相应的操作。它返回一个布尔值,如果对象是指定类的实例或实现了指定接口,则返回true
,否则返回false
。
默认字段初始化原则
如果写一个类时没有编写构造器,就会为你提供一个无参数构造器。这个构造器将所有的实例字段设置为默认值。于是,实例字段中的数值型数据设置为0
,布尔型数据设置为false
,所有对象变量将设置为null
。
示例类:
public class JavaInitTest {
private int id;
private String name;
private LocalDate constructTime;
}
使用无参构造器进行初始化
public JavaInitTest(){
id = 0;
name = "";
constructTime = null;
}
在类中进行显式字段初始化
public class JavaInitTest {
private int id = 0;
private String name = "";
private LocalDate constructTime = LocalDate.now();
}
使用字段初始化块(代码块)
在代码块中的内容将会在类实例化的时候先于构造器执行,并且使用类的静态字段时,此代码块并不会被执行
public class JavaInitTest {
private int id;
private String name;
private LocalDate constructTime;
{
id = 0;
name = "Kun's FeFan";
constructTime = LocalDate.now();
}
}
静态字段的三种初始化方式
显式字段初始化
public class JavaInitTestOfStatic {
public static int count = 0;
}
注意:Java中静态字段同样满足默认初始化原则
静态方法初始化
public class JavaInitTestOfStatic {
public static int count;
static void setStaticField(){
count = 233;
}
public static void main(String[] args){
JavaInitTestOfStatic.setStaticField();
System.out.println(JavaInitTestOfStatic.count);
}
}
也可以这样写:
package ClassVar;
public class JavaInitTestOfStatic {
public static int count = getVal();
public static int getVal(){
return 233;
}
public static void main(String[] args){
System.out.println(JavaInitTestOfStatic.count);
}
}
静态初始化块
在静态初始化代码块中的内容将会在类加载的时候被执行
public class JavaInitTestOfStatic {
public static int count;
static {
count = 233;
}
public static void main(String[] args){
System.out.println(JavaInitTestOfStatic.count);
}
}
Java中类会在哪些情况下被加载
调用类的静态方法时加载
package ClassVar;
public class MainClass {
public static int id;
static {
id = 233;
System.out.println("The field ID in the class is set to 233");
}
public static void main(String[] args) {
System.out.println("id = " + id);
}
}
类进行实例化时被加载
package ClassVar;
class MyTestClass{
public static int id;
static {
id = 233;
System.out.println("The field ID in the MyTestClass is set to 233");
}
}
public class MainClass {
public static void main(String[] args) {
MyTestClass test_obj = new MyTestClass();
System.out.println("id = " + test_obj.id);
}
}
父类在子类实例化时被加载
注意:父类会优先于子类加载
package ClassVar;
class MyTestClass{
public static int id;
static {
id = 233;
System.out.println("The field ID in the MyTestClass is set to 233");
}
}
class MySubClass extends MyTestClass{
static {
System.out.println("The class MySubClass has been loaded");
}
}
public class MainClass {
public static void main(String[] args) {
MyTestClass test_obj = new MySubClass();
System.out.println("id = " + test_obj.id);
}
}
创建对象时字段的加载顺序
- 首先被加载的是静态字段与静态初始化块,该两者处于同等地位,并且加载顺序取决于定义顺序
- 第二个被加载的是普通字段与普通初始化块,该两者也处于同等地位,并且加载顺序取决于定义顺序
- 其次被加载的是该类的构造器
package LoadTest;
class StaticContentLoad{
public static int num1 = getStaticVar();
static {
System.out.println("Static-Code-Block has been executed");
}
{
System.out.println("Normal-Code-Block has been executed");
}
public int num3 = getNormalVar();
public static int num2 = getStaticVar();
public static int getStaticVar(){
System.out.println("Method getStaticVar has been executed");
return 233;
}
public static int getNormalVar(){
System.out.println("Method getNormalVar has been executed");
return 2333;
}
public StaticContentLoad(){
System.out.println("Constructor has been executed");
}
}
public class EnterMain {
public static void main(String[] args) {
new StaticContentLoad();
}
}
得到以下运行结果:
Method getStaticVar has been executed
Static-Code-Block has been executed
Method getStaticVar has been executed
Normal-Code-Block has been executed
Method getNormalVar has been executed
Constructor has been executed
进程已结束,退出代码为 0
执行构造器前的操作
从上一节的内容不难看出,在执行当前类的构造器中的逻辑前,实质上还进行了其他操作,已知Java中的所有类均隐式继承自Object超类,则实际上在构造器中的代码前面其实**隐式执行了父类的无参构造器(super)**和当前类的普通初始化块,如果有继承自的父类将会依次执行:
package ClassOrderTest;
class MyClassA{
public MyClassA(){
System.out.println("Constructor of MyClassA has been executed");
}
}
class MyClassB extends MyClassA{
{
System.out.println("Normal-Code-Block of MyClassB has been executed");
}
public MyClassB(){
System.out.println("Constructor of MyClassB has been executed");
}
}
public class EnterMain {
public static void main(String[] args) {
new MyClassB();
}
}
继承下的加载顺序
- 先加载父类的静态代码块与静态字段,然后加载子类的静态代码块与静态字段
- 第二步加载父类的普通字段与普通代码块
- 第三步执行父类的构造器
- 最后加载子类的普通字段与普通代码块,然后执行子类的构造器
package ClassOrderTest;
class MyClassA{
static {
System.out.println("Static-Code-Block of MyClassA has been executed");
}
{
System.out.println("Normal-Code-Block of MyClassA has been executed");
}
public MyClassA(){
System.out.println("Constructor of MyClassA has been executed");
}
}
class MyClassB extends MyClassA{
static {
System.out.println("Static-Code-Block of MyClassB has been executed");
}
{
System.out.println("Normal-Code-Block of MyClassB has been executed");
}
public MyClassB(){
System.out.println("Constructor of MyClassB has been executed");
}
}
public class EnterMain {
public static void main(String[] args) {
new MyClassB();
}
}
运行结果如下:
Static-Code-Block of MyClassA has been executed
Static-Code-Block of MyClassB has been executed
Normal-Code-Block of MyClassA has been executed
Constructor of MyClassA has been executed
Normal-Code-Block of MyClassB has been executed
Constructor of MyClassB has been executed
进程已结束,退出代码为 0
abstract抽象类实现
抽象类(Abstract Class)是 Java 中一种特殊的类,它不能被实例化,主要用于作为其他类的基类。抽象类可以包含抽象方法和具体方法。抽象方法是一种没有具体实现的方法,它需要在子类中被具体实现。
类似C++的含纯虚函数类
抽象类的定义
使用 abstract
关键字定义抽象类,同时可以包含抽象方法和具体方法。抽象方法用 abstract
关键字声明,不包含方法体。
abstract class Shape {
// 抽象方法,没有方法体,需要在子类中实现
abstract void draw();
// 具体方法,可以在抽象类中有具体实现
void resize() {
System.out.println("Resizing shape");
}
}
抽象类的示例
class Circle extends Shape {
// 实现抽象方法
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Square extends Shape {
// 实现抽象方法
@Override
void draw() {
System.out.println("Drawing a square");
}
}
抽象类的使用
public class Main {
public static void main(String[] args) {
// 无法实例化抽象类
// Shape shape = new Shape(); // 编译错误
// 可以使用抽象类的引用指向其子类的对象
Shape circle = new Circle();
circle.draw(); // 输出: Drawing a circle
circle.resize(); // 输出: Resizing shape
Shape square = new Square();
square.draw(); // 输出: Drawing a square
square.resize(); // 输出: Resizing shape
}
}
抽象类的使用注意事项
-
无法实例化: 抽象类不能被直接实例化,只能用于被继承。
-
子类实现抽象方法: 如果一个类继承了抽象类,它必须实现抽象类中所有的抽象方法,除非该子类也是抽象类。
-
抽象类可以包含具体方法: 抽象类中可以包含具体方法(有方法体的方法),子类可以选择性地重写这些方法。
-
抽象类可以没有抽象方法: 抽象类可以不包含抽象方法,但包含抽象方法的目的是为了强制子类实现特定的方法。
-
抽象类的构造方法: 抽象类可以有构造方法,但不能被直接实例化。子类在实例化时会调用父类的构造方法。
使用抽象类的主要目的是为了定义一个通用的类结构,让子类去具体实现其中的抽象方法。
interface接口概念
在Java程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。
form: Java核心技术·卷 I(原书第11版) 基础知识 by 凯 S.霍斯特曼
在Java中,接口(Interface)是一种抽象类型,它定义了一组方法的签名(方法名、返回类型、参数列表)但没有提供方法的实现,即接口不能够被实例化。类实现(implements)接口,从而承诺提供接口定义的所有方法的实现。接口提供了一种方式来实现多重继承,因为一个类可以实现多个接口。
为何使用接口编程
接口编程是一种促进良好软件设计实践的方法,可以改善代码的结构、可维护性和可扩展性。通过定义清晰的接口,我们能够更好地组织和管理复杂的软件系统,使用接口主要有以下几种原因:
-
抽象和解耦: 接口提供了一种抽象层,使得实现和使用代码可以相互独立。通过定义接口,我们可以将系统的不同组件解耦,降低它们之间的依赖关系。这有助于提高代码的模块性和可维护性。
-
多态性: 接口允许多态性,即一个对象可以以多种形式存在。通过使用接口,我们可以编写可以操作不同类型对象的通用代码,从而增加了代码的灵活性和可扩展性。
-
规范标准: 接口定义了一组规范和标准,用于描述类或模块的行为。这有助于团队协作,因为开发人员可以遵循相同的接口规范,确保代码的一致性和可预测性。
-
测试和调试: 接口使得单元测试更加容易。通过编写适当的接口,可以使用模拟对象或模拟接口来测试代码的不同部分,而无需实际实现所有相关的类或模块。
-
提高可替代性: 通过使用接口,我们可以在不影响整个系统的情况下替换具体的实现。这使得在不修改调用方代码的情况下更容易切换实现,从而提高系统的可替代性。
-
降低耦合度: 接口编程有助于减少代码之间的紧密耦合。这意味着当一个模块发生变化时,其他模块不太可能受到影响,从而提高了代码的可维护性和稳定性。
-
提高代码的可读性和理解性: 接口提供了一种清晰的契约,描述了类或模块的期望行为。这有助于提高代码的可读性,并使其他开发人员更容易理解和使用你的代码。
定义接口类型
注意:接口的访问控制符只能是包默认(default)和
public
两种类型
使用 interface
关键字来定义接口。接口可以包含方法签名、常量(静态 final 域),但不能包含字段(成员变量)或实现方法的代码块,并且接口中的所有方法都是自动视为public
的,故不需要书写public
,并且接口中的常量会被默认置为public static final
,并且必须进行初始化操作
public interface MyInterface {
// 接口方法的签名 不需要实现
void myMethod();
// 接口中的常量 隐式声明为 public static final
int MY_CONSTANT = 42;
}
在上述接口中,我们定义了一个返回值为void,无需形参的方法签名,该方法本质上是abstract
的,只是在接口实现中,未实现方法默认abstract
,所以不用书写关键字,当然可以加上显式声明,不过并不推荐:
接口默认方法(Java 8+)
在Java8
之后的版本,接口可以包含默认方法,这是在接口中提供方法实现的新特性。实现类可以选择重写默认方法。
public interface MyInterface {
// 抽象方法
void myMethod();
// 默认方法
default void defaultMethod() {
System.out.println("Default implementation of defaultMethod");
}
}
静态方法(Java 8+):
在Java8
之后的版本,接口可以包含静态方法,这是在接口级别提供工具方法的一种方式,即可以通过接口名调用静态方法。
public interface MyInterface {
// 静态方法
static void staticMethod() {
System.out.println("Static method in interface");
}
}
实现接口
警告:在接口声明中,没有将类中方法声明为
public
,这是因为在接口中的所有方法都自动是public
。不过,在实现接口时,必须把方法声明为public
;否则,编译器将认为这个方法的访问属性是包可见性,这是类的默认访问属性,之后编译器就会报错,指出你试图提供更严格的访问权限。from: Java核心技术·卷 I(原书第11版) 基础知识 by 凯 S.霍斯特曼 page 224(根据博文进行了简单修改)
抽象类实现
抽象类实现接口,可以不扩展方法
interface InterfaceOne{
void occupyLocationFunc();
}
abstract class AbstractClass implements InterfaceOne{
// code
}
单接口实现
在类使用 implements
关键字来实现接口。实现接口的可实例化类必须提供接口中定义的所有方法的具体实现。
public class MyClass implements MyInterface {
// 实现接口中定义的方法
@Override
public void myMethod() {
// 具体的实现逻辑
System.out.println("Implementation of myMethod and MY_CONSTANT is " + MY_CONSTANT);
}
}
注意:在外部访问接口中的常量:
InterfaceName.constant_name // 接口名.常量名
多接口实现
一个类可以实现多个接口,通过使用逗号分隔接口名称。
public class MyMultiInterfaceClass implements Interface1, Interface2 {
// 实现接口中定义的方法
// ...
}
多接口实现时指定
在 Java 中,一个类可以实现多个接口,而这些接口可能包含相同签名的默认方法。当一个类实现多个接口,并且这些接口中有相同签名的默认方法时,需要使用
super
关键字明确指定调用哪个接口的默认方法。
考虑以下的例子:
interface InterfaceA {
default void defaultMethod() {
System.out.println("Default method in InterfaceA");
}
}
interface InterfaceB {
default void defaultMethod() {
System.out.println("Default method in InterfaceB");
}
}
class MyClass implements InterfaceA, InterfaceB {
@Override
public void defaultMethod() {
InterfaceA.super.defaultMethod(); // 明确指定调用 InterfaceA 的默认方法
InterfaceB.super.defaultMethod(); // 明确指定调用 InterfaceB 的默认方法
System.out.println("Overridden default method in MyClass");
}
}
public class Main {
public static void main(String[] args) {
MyClass myObject = new MyClass();
myObject.defaultMethod();
}
}
在这个例子中,MyClass
类实现了两个接口 InterfaceA
和 InterfaceB
,并且这两个接口都定义了相同签名的默认方法 defaultMethod
。在 MyClass
中,通过使用 InterfaceA.super.defaultMethod()
和 InterfaceB.super.defaultMethod()
明确指定调用哪个接口的默认方法。
这种情况下,使用 super
关键字的语法为:
InterfaceName.super.defaultMethod();
其中,InterfaceName
是接口的名称。这样就能够避免默认方法冲突的问题,同时在实现类中提供自定义的实现。
接口继承
接口可以扩展其他接口,使用 extends
关键字。
public interface ExtendedInterface extends MyInterface {
// 可以定义新的方法或使用继承的方法
// ...
}
接口演化
接口演化是指的在接口的第一个版本实现后,又增加了新的方法签名(先允许我这样说,那么会产生一个问题,我们在原有接口基础上增加一个方法签名:
public interface MyInterface {
int MY_CONSTANT = 42;
void myMethod();
void printHelloWorldForUser(String user_name);
}
假如在此之前我已经实现了一个类:
public class MyClass implements MyInterface {
@Override
public void myMethod() {
System.out.println("Implementation of myMethod and MY_CONSTANT is " + MY_CONSTANT);
}
}
那么增加过后,我的类将无法编译,因为我并未提供完整的接口实现,void printHelloWorldForUser(String user_name)
并没有实现(已经编译好的不受影响),所以无法编译,此时就需要我们的默认方法实现:
public interface MyInterface {
int MY_CONSTANT = 42;
void myMethod();
default void printHelloWorldForUser(String user_name){
System.out.println("Hi " + user_name + " Hello World");
}
}
即使用默认方法以保证源代码兼容
接口与继承同时实现
在类实现接口时,也可以从超类进行继承,不过继承必须写在前面,否则会报错:
abstract class MyBaseClass{
abstract void myBaseClassFunc();
}
public class MyClass extends MyBaseClass implements MyInterface {
@Override
public void myBaseClassFunc(){
System.out.println("This is the base class method implementation");
}
@Override
public void myMethod() {
System.out.println("Implementation of myMethod and MY_CONSTANT is " + MY_CONSTANT);
}
}
接口与抽象类的差异
由于Java中不能使用像C++中的多重继承,因为Java设计者认为多重继承会使得代码过于复杂,抽象类由于每次只能扩展一个类,其无法满足多扩展的需求
C++注释:C++具有多重继承特性,随之带来了一些复杂的特性,如虚基类、控制规则和横向指针类型转换,等等。很少有C++程序员使用多重继承,甚至有些人说就不应该使用多重继承。也有些程序员建议只对“混合”风格的继承使用多重继承。在“混合”风格中,一个主要基类描述父对象,其他的基类(所谓的混合类)提供辅助特性。这种风格类似于一个Java类扩展一个基类且派生并实现多个接口。
from: Java核心技术·卷 I(原书第11版) 基础知识 by 凯 S.霍斯特曼 page 230
解决默认方法冲突
- 超类优先:如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
public interface MyInterface {
default void printHello(){
System.out.println("Hello World");
}
}
class BaseClass{
public void printHello(){
System.out.println("Hello Java");
}
}
public class SubClass extends BaseClass implements MyInterface {
public static void main(String[] args){
SubClass object = new SubClass();
object.printHello();
}
}
- 接口冲突:如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
public interface MyInterfaceOne {
default void printHello(){
System.out.println("Hello World");
}
}
interface MyInterfaceTwo{
default void printHello(){
System.out.println("Hello Java");
}
}
public class SubClass implements MyInterfaceOne, MyInterfaceTwo {
@Override
public void printHello() {
MyInterfaceOne.super.printHello();
// System.out.println("Hello IDEA");
}
public static void main(String[] args){
SubClass object = new SubClass();
object.printHello();
}
}
接口的回调操作
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。例如,按下鼠标或选择某个菜单项时,你可能希望完成某个特定的动作。
form: Java核心技术·卷 I(原书第11版) 基础知识 by 凯 S.霍斯特曼 page 233
我们通过模拟日常生活中数码产品USB接入电脑的操作来模拟接口回调:
- 定义接口
USBInterface
并定义工作状态方法签名
interface USBActionable {
void workingStart();
void workingStop();
}
- 实现接口扩展不同工作状态
class Phone implements USBActionable {
@Override
public void workingStart(){
System.out.println("The phone usb interface work starts");
}
@Override
public void workingStop(){
System.out.println("The phone usb interface stops working");
}
}
class Camera implements USBActionable {
@Override
public void workingStart(){
System.out.println("The camera usb interface work starts");
}
@Override
public void workingStop(){
System.out.println("The camera usb interface stops working");
}
}
- 在
Computer
中定义进行USB接入时的回调行为(这里为了演示方便,定义为静态方法)
class Computer{
public static void printWorkingStatus(USBActionable other){
other.workingStart();
other.workingStop();
}
}
- 在
Main
类中运行
public class Main {
public static void main(String[] args){
Phone phone = new Phone();
Camera camera = new Camera();
Computer.printWorkingStatus(phone);
Computer.printWorkingStatus(camera);
}
}
接口多态实现
基类引用实现运行时多态
在原有代码基础上进行修改:
public class Main {
public static void main(String[] args){
USBActionable phone = new Phone();
USBActionable camera = new Camera();
phone.workingStart();
camera.workingStart();
}
}
我们使用接口类型的变量引用对应的实现了接口的对象,运行时,会根据引用的对象来调用对应的方法
方法签名重载实现编译时多态
interface InterfaceOne{
void occupyLocationFunc();
void occupyLocationFunc(Object occupy_variable);
}
个人觉得这种东西区分一下即可,毕竟实现接口就已经重写覆盖了对应的方法,实质上也就是一种编译时多态
接口继承实现多态传递
interface InterfaceOne{
default void occupyLocationFunc(){
System.out.println("occupyLocationFunc of InterfaceOne");
};
}
interface InterfaceTwo extends InterfaceOne{
default void printHelloWorld(){
System.out.println("printHelloWorld of InterfaceTwo");
}
}
class TestClass implements InterfaceTwo{
}
public class ExtendsClassMain {
public static void main(String[] args) {
InterfaceOne one = new TestClass();
one.occupyLocationFunc();
}
}
在上述代码中,我们以最外层接口类型InterfaceOne
变量来指向引用的TestClass
对象,并调用其继承自InterfaceOne
的方法,实现了多态的传递
工程拓展-接口与API
身边很多人经常挂在嘴边,但是不明所以,所以一并写下
接口(Interface)和API(Application Programming Interface)是软件开发中经常使用的两个术语,它们有不同的含义和用法。
接口基本概念与特征
-
接口定义: 在面向对象编程中,接口是一种抽象类型,它定义了一组方法(或方法签名)而没有提供方法的具体实现。接口可以包含常量(静态不可变的变量),但通常主要包含方法声明。
-
实现接口: 类可以实现一个或多个接口,表示该类将提供接口中定义的所有方法的具体实现。通过实现接口,类可以达到多继承的效果。
-
Java中的接口: 在Java编程语言中,接口是通过
interface
关键字定义的,类通过implements
关键字来实现接口。接口的实例方法默认是public abstract
的。
public interface MyInterface {
void myMethod();
int myAnotherMethod();
}
API基本概念与特征
-
API定义: API(Application Programming Interface)是一组定义了软件组件之间交互的规范。它可以包含类、函数、协议、工具等,允许不同的软件系统之间进行交互和集成。
-
种类: API的种类很多,包括库级别的API、操作系统级别的API、网络API等。在编程中,常见的是库级别的API,用于与库或框架进行交互。
-
示例: Java API指的是Java编程语言提供的类和方法,而不同的库、框架、操作系统也都有各自的API。例如,Java提供的
java.util.List
接口和ArrayList
类就是Java API的一部分。
List<String> myList = new ArrayList<>();
myList.add("Hello");
myList.add("World");
区分接口与API
-
定义: 接口是一种抽象类型,定义了一组方法的声明而没有具体实现。API是一组规范,可以包括接口、类、函数等,用于软件组件之间的交互。
-
用途: 接口用于定义类的合同,规定了类应该提供哪些方法,而API用于描述各种软件组件之间的交互方式。
-
实现: 类可以实现一个或多个接口,但接口本身并不提供具体的实现。API可以包含具体的实现,例如库或框架提供的类和方法。
-
示例:
java.util.List
是一个接口,而Java API包含了许多类似ArrayList
的具体实现,以及其他各种类和方法。
总体而言,接口是一种抽象的概念,用于定义类的契约,而API是一组规范,用于描述软件组件之间的交互。在软件开发中,开发人员通常需要实现接口来满足一定的规范,并使用各种API来构建应用程序。
成员内部类使用案例
注意:内部类中的静态字段必须是
final
,并且初始化为一个编译时常量。内部类一般不拥有静态方法,有静态方法也只能够访问外部类的静态方法与静态字段
package InnerClass;
public class NormalOuterClass {
public int outerId = 2333;
class InnerClass{
public int innerId =233;
public int getInnerId() {
System.out.println(outerId);
return innerId;
}
}
public InnerClass getInnerObject(){
return new InnerClass();
}
public static void main(String[] args) {
// 使用NormalOuterClass.InnerClass访问内部类
NormalOuterClass.InnerClass myObject = new NormalOuterClass().getInnerObject();
System.out.println(myObject.getInnerId());
}
}
这种使用方式和CPP中的嵌套类是较为类似的,但是Java中的内部类是可以访问外部类成员的,而CPP则不能够直接访问
成员内部类比局部类更加直接,地位就相当于一个类的成员,它的使用有以下特点:
- 作为类的成员,它可以访问到外部类的所有成员
- 因为是外部类成员,它允许使用访问控制符来控制该成员内部类的可见性
- 外部类必须通过实例化该成员内部类才能够访问内部类成员,并且成员内部类也只能通过外部方法来初始化自身
内部类可访问外部字段原因
能够访问外部类的字段是因为内部类的对象含有一个隐式引用指向其对应的外部类对象,但是这个引用在内部类中是不可见的,并且编译器会修改内部类构造器使得其接受一个外部类引用,外部类方法构造该内部类后,对应的外部类this就会被传递给内部类构造器。我们可以查看编译后的class
文件内容:
Compiled from "NormalOuterClass.java"
class InnerClass.NormalOuterClass$InnerClass {
public int innerId;
final InnerClass.NormalOuterClass this$0;
InnerClass.NormalOuterClass$InnerClass(InnerClass.NormalOuterClass);
public int getInnerId();
}
发现该内部类对应的外部类对象隐式引用,并且是一个final
类型:
final InnerClass.NormalOuterClass this$0;
并且在内部类的构造器中增加了一个外部类对象引用参数:
InnerClass.NormalOuterClass$InnerClass(InnerClass.NormalOuterClass);
所以在构造内部类对象时也可以显式声明构造,其中的NormalOuterClass.this
表达式表示外部类引用:
NormalOuterClass.this.new InnerClass();
还有的文章中会提到access静态方法,不过我使用的JDK17通过反编译与反射都没看到,可能是版本问题
局部内部类使用案例
局部内部类定义在外部类的方法之中,它可以访问外部类中的所有字段,但是不能够添加类的访问控制符(final可以),其性质相当于一个局部变量,作用域局限在其对应的代码块中,并且也能访问所在代码块中的局部变量,但是外部类要访问内部类必须通过实例化调用方法:
public class OuterClass {
private String content = "Content of Class-OuterClass";
private static String staticContent = "staticContent of Class-OuterClass";
public void powerTest(){
System.out.println("Function of Class-OuterClass");
}
public void checkVisitPower(){
int localVarInFunc =233;
final class InnerClass{
private String content = "Content of Class-InnerClass";
public void showOuterInfo(){
System.out.println(content);
System.out.println(OuterClass.this.content);
System.out.println(staticContent);
powerTest();
System.out.println("localVarInFunc = "+ localVarInFunc);
}
}
var obj = new InnerClass();
obj.showOuterInfo();
}
public static void main(String[] args) {
var obj = new OuterClass();
obj.checkVisitPower();
}
}
注意:外部其他类不能够访问局部内部类,因为局部内部类仅仅相当于本外部类方法中的一个局部变量,外部类与内部类的成员重名时遵循就近原则,如果想要访问外部类中的同名成员需要借助外部类引用,如示例中的:
System.out.println(OuterClass.this.content);
匿名内部类使用案例
匿名局部类是一种没有显式名称的局部内部类。它通常用于创建只需要使用一次的类的实例。匿名局部类通常用于实现接口或继承抽象类,可以在创建实例的同时提供实现。在许多框架中是比较常见的,创建匿名内部类时需要注意编译类型与运行类型。
interface AnonymityTestInterface {
void startTestFunction();
}
public class AnonymityTestMain{
public static void main(String[] args) {
// 编译类型为AnonymityTestInterface
// 运行类型为AnonymityTestMain$1 此名称为JVM自动分配
AnonymityTestInterface objectTest = new AnonymityTestInterface() {
@Override
public void startTestFunction() {
System.out.println("This is a AnonymityClass");
}
};
objectTest.startTestFunction();
System.out.println(objectTest.getClass());
}
}
可见我们在上述代码中实际上借助匿名内部类实现了接口,即相当于在局部实现了:
class AnonymityTestMain$1 implements AnonymityTestInterface
对于普通类和抽象类我们可以像如下这样建立匿名类,并且执行重写或增加方法(这里以普通类进行演示):
class BaseClass{
protected int id = 233;
public int getId() {
return id;
}
}
public class AnonymityTestMain{
public static void main(String[] args) {
// 编译类型为BaseClass
// 运行类型为AnonymityTestMain$1
BaseClass objectTest = new BaseClass(){
@Override
public int getId(){
System.out.println("Id of AnonymityClass is : " + id);
this.checkAddFunction();
return id;
}
public void checkAddFunction(){
System.out.println("The checkAddFunction has been started");
}
};
System.out.println(objectTest.getId());
}
}
和前面接口处是类似的,其语法层面相当于:
class AnonymityTestMain$1 extends BaseClass
需要强调的是,在添加了额外方法的匿名类中,通常情况下是无法在外部直接访问新增的方法的,因为其编译类型仍然是父类,即根据继承中的向上转型原则,父类的引用是无法直接访问到子类的新增方法的。
还需要注意的是,这里的自动生成的一个类名
AnonymityTestMain$1
,它反映的实质上是Java中的动态绑定过程,Java默认情况下所有的方法都是动态绑定的,也就是说Java中的方法调用是在运行时确定的,所以getClass
获取到的元数据来自于内部类,而不是外部的BaseClass
由于匿名类没有类名,所以也不能在构造中传递参数,但是仍然需要保留
()
匿名内部类的双重特性
匿名内部类是一种没有显式定义类名的内部类,它同时具有类和对象的特性。在Java中,匿名内部类可以看作是一个对象,因为它可以实现接口或继承一个类,并且可以被赋值给一个变量。同时,匿名内部类也可以看作是一个类,因为它可以拥有方法和字段,并且可以在代码中被实例化和使用。
上述代码中我们实质上已经体现出了一个匿名内部类的两大特性:
- 类方面的特性,因为它可以进行重写类方法等操作
- 对象的特性,因为它可以被变量所接收,并在下文根据变量来调用方法
不过我们还应该注意一种写法,我们实质上是可以直接在内部类的后面直接调用方法的:
public class Main{
public static void main(String[] args) {
int objectId = new BaseClass(){
@Override
public int getId(){
System.out.println("Id of AnonymityClass is : " + id);
return id;
}
}.getId();
System.out.println(objectId);
}
}
静态内部类使用案例
public class OuterClass {
private static int outerStaticField = 1;
private int outerField = 2;
// 静态内部类
public static class StaticInnerClass {
public void display() {
// 无法直接访问外部类的非静态成员,需要通过实例访问
OuterClass outer = new OuterClass();
System.out.println("OuterField: " + outer.outerField);
System.out.println("OuterStaticField: " + outerStaticField);
}
}
public static void main(String[] args) {
StaticInnerClass inner = new StaticInnerClass();
inner.display();
}
}
-
静态内部类可以脱离外部类的实例单独存在,与外部类实例无关。
-
静态内部类可以访问外部类的静态成员和方法,但不能直接访问外部类的实例变量和实例方法,需要通过实例化外部类对象来访问。
-
静态内部类的命名空间独立于外部类,可以有相同名称的静态内部类存在,互不影响。
-
静态内部类可以直接通过
new OuterClass.StaticInnerClass()
来实例化,无需先实例化外部类。 -
静态内部类的生命周期可以超出外部类,即使外部类实例被销毁,静态内部类的实例仍然存在。
-
静态内部类可以访问外部类的静态成员,因此可以实现资源共享和代码重用的功能。
八大包装类
Java的包装类(Wrapper Class)是一种将基本数据类型封装成对象的类。在Java中,基本数据类型(如int、double、char等)不是对象,无法直接参与面向对象的操作,因此引入了包装类来解决这个问题。包装类提供了一些方法来处理基本数据类型的值,使其可以像对象一样进行操作。并且在这八大包装类中除了Character
,Boolean
类其他均继承自Number
类。
包装类 | 基本数据类型 | 常用方法 |
---|---|---|
Integer | int | parseInt(String s) , valueOf(int i) |
Long | long | parseLong(String s) , valueOf(long l) |
Float | float | parseFloat(String s) , valueOf(float f) |
Double | double | parseDouble(String s) , valueOf(double d) |
Boolean | boolean | parseBoolean(String s) , valueOf(boolean b) |
Byte | byte | parseByte(String s) , valueOf(byte b) |
Short | short | parseShort(String s) , valueOf(short s) |
Character | char | toString(char c) , valueOf(char c) |
parseXXX
方法通常用于从String类型中解析构建对应的包装类对象valueOf
方法通常用来将对应的基本类型进行包装而得到包装类对象
包装类的装箱与拆箱操作
Java提供了装箱(Boxing)、拆箱(Unboxing)、自动拆箱(Autounboxing)和自动装箱(Autoboxing)的功能用来实现基本类型与包装类对象的转换。
- 装箱(Boxing):将基本数据类型转换为对应的包装类对象。例如,将
int
转换为Integer
,将double
转换为Double
等。这样就可以将基本数据类型作为对象使用,可以调用对象的方法、存入集合类中等。
int num = 10;
Integer obj = Integer.valueOf(num); // 装箱 从基础类型进行包装
- 拆箱(Unboxing):将包装类对象转换为对应的基本数据类型。例如,将Integer转换为int,将Double转换为double等。
Integer obj = new Integer(10);
int num = obj.intValue(); // 拆箱 从包装类中取得基础类型值
- 自动装箱(Autoboxing)与自动拆箱(Autounboxing):Java 5引入的特性,可以自动将基本数据类型转换为对应的包装类对象(其底层调用的仍然是
valueOf
方法)。反之,也可以自动将包装类对象转换为基本数据类型(底层调用的是xxxValue
方法)。
int num = 10;
Integer obj = num; // 自动装箱
Integer obj = new Integer(10);
int num = obj; // 自动拆箱
包装类与String间的转化行为
public class WrapperExtends {
public static void main(String[] args) {
int baseIntValue = 233;
Integer ExIntValue = baseIntValue;
String intString = "2333";
String strOfInteger = ExIntValue.toString();
Integer ExIntOfString = Integer.valueOf(intString);
String strBySelfFunc = String.valueOf(ExIntValue);
String autoIntegerToStr = ExIntValue + "333";
Integer strByParse = Integer.parseInt(intString);
}
}
代码 | 描述 |
---|---|
Integer ExIntValue = baseIntValue; | 通过Integer 类的构造方法将基本数据类型int 转换为包装类Integer 。 |
String strOfInteger = ExIntValue.toString(); | 通过Integer 类的toString() 方法将包装类Integer 转换为对应的字符串。 |
Integer ExIntOfString = Integer.valueOf(intString); | 通过Integer 类的valueOf() 方法将字符串转换为包装类Integer 。 |
String strBySelfFunc = String.valueOf(ExIntValue); | 通过String 类的valueOf() 方法将包装类Integer 转换为对应的字符串。 |
String autoIntegerToStr = ExIntValue + "333"; | 通过Integer 类的自动拆箱与装箱功能,将包装类Integer 自动拆箱为int ,然后与String 相加,再转换为String 。 |
Integer strByParse = Integer.parseInt(intString); | 通过Integer 类的parseInt() 方法将字符串转换为int 类型并自动装箱为Integer 对象。 |
需要注意的是在转化时,我们需要注意一个包装类中的两个字段(下面是Intger
源码中的字段):
@Native public static final int MIN_VALUE = 0x80000000;
@Native public static final int MAX_VALUE = 0x7fffffff;
这两个字段指明了对应包装类可包装的值的最大值与最小值,并且在其他继承自Number的包装类中均有实现(下面是部分视图):
注意,由于使用构造函数构建Integer的方式不常使用,在JDK17中显示标记为弃用:
Integer intByConstruct = new Integer(baseIntValue); Integer strByConstruct = new Integer(intString);
Integer的valueOf(int)转化
在面试题中出现过,故此增加此小节
在Interger
的valueOf
对int
类型转化的方法实现中,它会对传入值进行一个判断,返回不同的类型:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这里的cache
实质上是来自于IntegerCache
类,在它的实现中会进行一系列的判断,简而言之在 -128
到127
之间的值将会返回原本的int
类型值,只有超出这个范围的值,且在Integer
划定范围内的值才能被转化为Integer
类型的对象
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
...
}
运行以下代码将会得到以下结果:
public class WrapperExtends {
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
System.out.println(a == b);
Integer c = 128;
Integer d = 128;
System.out.println(c == d);
}
}
true
false
这里需要强调的是,在Integer与Int基本类型值进行比较时进行比较的是其包装的值,因为在进行比较时会进行自动拆箱操作。
自定义枚举体案例
class SeasonEnum {
public static final SeasonEnum SPRING = new SeasonEnum("spring", "Mild and blooming");
public static final SeasonEnum SUMMER = new SeasonEnum("summer", "Hot and sunny");
public static final SeasonEnum AUTUMN = new SeasonEnum("autumn", "Cool and colorful");
public static final SeasonEnum WINTER = new SeasonEnum("winter", "Cold and snowy");
private final String season;
private final String describe;
private SeasonEnum(String seasonName, String aDescribe){
season = seasonName;
describe = aDescribe;
}
@Override
public String toString() {
return "The " + season + " is " + describe;
}
}
实质上是借助静态成员实现的枚举体
标准枚举体案例
标准枚举体实质上就是对上述的定义进行了简化,并且需要强调的是:
在枚举体中,构造函数默认是私有的,因此在枚举体中显式使用 private
修饰符来声明构造函数是冗余的。这是因为枚举体的设计初衷就是为了限制创建枚举实例的方式,只能通过枚举常量来创建实例,而不能通过其他方式。
enum
关键字声明的类隐式继承自Java的Enum
类(不可再继承其他类),并自动成为一个final类。在书写时,作为枚举的字段必须放在最前面,每项用,
分割,最后一项以;
收尾
但是值得注意的是enum类虽然不能够再继承其他类,但是却可以实现接口
enum SeasonEnumStandard{
// 注意 作为枚举内容时将会调用类的构造器
SPRING ("spring", "Mild and blooming"),
SUMMER ("summer", "Hot and sunny"),
AUTUMN ("autumn", "Cool and colorful"),
WINTER ("winter", "Cold and snowy");
private final String season;
private final String describe;
SeasonEnumStandard(String seasonName, String aDescribe){
season = seasonName;
describe = aDescribe;
}
@Override
public String toString() {
return "The " + season + " is " + describe;
}
}
在构造函数是无参数时,我们可以直接省略
()
其他枚举体中的方法使用案例:
public class EnumStandard {
public static void main(String[] args) {
// 获取枚举值的名字
System.out.println(SeasonEnumStandard.SPRING.name());
// 获取枚举值在序列中的定义数 0为起始值
System.out.println(SeasonEnumStandard.AUTUMN.ordinal());
// 获取枚举数序列中所有枚举值的内容
for (var value: SeasonEnumStandard.values()){
System.out.println(value);
}
System.out.print("\n");
// 将String转换为枚举值 不在枚举序列中的值将会抛出异常
System.out.println(SeasonEnumStandard.valueOf("SUMMER"));
// 将两个枚举值进行序列比较 返回当前枚举值与目标枚举值的差值
System.out.println(SeasonEnumStandard.SPRING.compareTo(SeasonEnumStandard.WINTER));
}
}
注解作用与基本注解
在Java中,注解(Annotation)是一种用来为程序元素(类、方法、变量等)添加元数据(metadata)的形式化标签。它们提供了有关程序的额外信息,可以用于编译时进行检测、运行时处理或者在编译和部署阶段生成代码。
在Java SE(Standard Edition)中,注解可以用于为代码提供额外的信息,例如告诉编译器如何处理类、方法、变量等。常见的注解包括 @Override
、@Deprecated
和 @SuppressWarnings
等。
在Java EE(Enterprise Edition)中,注解扮演着更为重要的角色,它们被广泛用于标记和配置应用程序中的各种组件,如Servlet、EJB、JPA实体等。这些注解可以简化开发人员对应用程序的配置和管理,提高了代码的可读性和可维护性。
Java SE中的注解主要用于提供编译时的辅助信息,而Java EE中的注解则更多地用于配置和管理应用程序的各个组件。
Override注解
@Override
:用于标识一个方法覆盖了父类的方法,且只能用于方法。使用此注解后,编译器将会进行校验,以确定是否真的进行了重写,从而避免潜在错误。
class Parent {
public void print() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
public void print() {
System.out.println("Child");
}
}
@interface注解类
注意在@Override的源码中出现的@interface:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
在这里,@interface
是用来声明一个注解的关键字,表示我们正在定义一个新的注解类。@Target
表示这个注解可以用在方法上,@Retention
表示它的保留策略(Retention Policy)被设置为 SOURCE,这意味着编译器会在编译时丢弃这个注解,它不会被包含在编译后的 class 文件中。
Deprecated注解
@Deprecated
:用于标识某个程序元素(类、方法等)已经过时,不推荐使用。
public class DeprecatedExample {
@Deprecated
public void oldMethod() {
System.out.println("This method is deprecated.");
}
}
@Deprecated
class DeprecatedClass {
public void deprecatedMethod() {
System.out.println("This method is deprecated.");
}
}
- Deprecated注解源码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
/**
* Returns the version in which the annotated element became deprecated.
* The version string is in the same format and namespace as the value of
* the {@code @since} javadoc tag. The default value is the empty
* string.
*
* @return the version string
* @since 9
*/
String since() default "";
/**
* Indicates whether the annotated element is subject to removal in a
* future version. The default value is {@code false}.
*
* @return whether the element is subject to removal
* @since 9
*/
boolean forRemoval() default false;
}
-
@Documented
: 这是一个元注解,用于指示注解应该包含在生成的文档中。当一个注解被标记为@Documented
时,它的信息会被包含在 Javadoc 文档中,使得注解的信息可以被文档化。 -
@Retention(RetentionPolicy.RUNTIME)
用于指定注解的保留策略。在这里,@Deprecated
注解被指定为在运行时保留,这意味着可以通过反射等机制在运行时获取到这个注解的信息。 -
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
用于指定注解可以应用的目标元素类型。在这里,@Deprecated
注解可以应用在构造方法、字段、局部变量、方法、包、模块、参数和类型上。 -
public @interface Deprecated { ... }
定义了@Deprecated
注解的具体内容。在这个注解中,包含了两个成员方法since()
和forRemoval()
,分别用于指定元素被标记为过时的版本和标记元素是否将来会被移除。这些信息可以帮助开发者了解过时元素的相关情况。
SuppressWarning注解
@SuppressWarnings
:用于抑制编译器警告。
public class SuppressWarningsExample {
@SuppressWarnings({"unchecked", "rawtypes"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("Hello");
list.add("World");
// 使用了原始类型的 List,会导致 "unchecked" 警告
// 使用了未检查的转换,会导致 "rawtypes" 警告
// 使用 @SuppressWarnings({"unchecked", "rawtypes"}) 可以抑制这两种警告
}
}
@SuppressWarnings
注解可以用来抑制特定类型的警告,其作用范围可以是单个变量、方法、类、或者整个方法体。具体来说,@SuppressWarnings
的作用范围取决于它所放置的位置:
单个变量或表达式:可以将 @SuppressWarnings
注解直接放置在变量或表达式之前,例如:
@SuppressWarnings("unchecked")
List<String> list = new ArrayList();
方法或构造函数:可以将 @SuppressWarnings
注解放置在方法或构造函数的声明上,例如:
@SuppressWarnings("unchecked")
public void myMethod() {
List<String> list = new ArrayList();
}
类:可以将 @SuppressWarnings
注解放置在类的声明上,以抑制类中所有方法的警告,例如:
@SuppressWarnings("unchecked")
public class MyClass {
// 类的所有方法都将抑制 "unchecked" 警告
}
方法体:可以将 @SuppressWarnings
注解放置在方法体内的局部变量或表达式之前,例如:
public void myMethod() {
@SuppressWarnings("unchecked")
List<String> list = new ArrayList();
}
需要注意的是,@SuppressWarnings
注解只对编译器有效,不会影响程序运行时的行为。使用时应尽量精确指定需要抑制的警告类型,避免过度使用导致隐藏潜在问题。
- SuppressWarning注解源码
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The set of warnings that are to be suppressed by the compiler in the
* annotated element. Duplicate names are permitted. The second and
* successive occurrences of a name are ignored. The presence of
* unrecognized warning names is <i>not</i> an error: Compilers must
* ignore any warning names they do not recognize. They are, however,
* free to emit a warning if an annotation contains an unrecognized
* warning name.
*
* <p> The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
String[] value();
}
- SuppressWarning注解支持的标记列表
警告类型 | 描述 |
---|---|
“all” | 抑制所有类型的警告 |
“rawtypes” | 抑制使用原始类型(raw types)的警告 |
“unchecked” | 抑制未经检查的转换警告 |
“deprecation” | 抑制使用过时 API 的警告 |
“removal” | 抑制对已删除 API 的警告 |
“serial” | 抑制没有序列化 ID 的类实现 Serializable 接口的警告 |
“finally” | 抑制没有返回或抛出异常的 finally 块的警告 |
“fallthrough” | 抑制 switch 语句中缺少 break 的警告 |
“path” | 抑制在类路径中找不到的类、包或资源的警告 |
“unchecked” | 抑制执行了未检查操作的警告(例如使用原始类型集合) |
“rawtypes”,“unchecked” | 抑制所有与未检查操作相关的警告 |
“static-access” | 抑制对静态成员的访问警告 |
“unused” | 抑制未使用的代码的警告 |
“unused”,“rawtypes” | 抑制未使用的代码和使用原始类型的警告 |
“unused”,“unchecked” | 抑制未使用的代码和未经检查的转换的警告 |
元注解概念
Java 的元注解(meta-annotation)是一种用于注解其他注解的注解。元注解可以用来为注解提供更多的元数据信息,例如指定注解的保留策略、目标元素类型等。Java 中有几种常见的元注解,包括 @Retention
、@Target
、@Documented
和 @Inherited
等。
元注解 | 功能描述 |
---|---|
@Retention | 指定注解的保留策略,即注解在什么时候生效。可选值有 RetentionPolicy.SOURCE(源代码级别)、RetentionPolicy.CLASS(类文件级别)和 RetentionPolicy.RUNTIME(运行时级别)。 |
@Target | 指定注解可以应用的目标元素类型,如类、方法、字段等。 |
@Documented | 指定注解是否包含在 Javadoc 中。如果一个注解被 @Documented 修饰,那么它将会出现在生成的文档中。 |
@Inherited | 指定注解是否可被继承,默认情况下注解不会被继承。如果一个注解被 @Inherited 修饰,那么它将会被子类继承。 |