一、面向对象三大特征
可以看一下这篇文章:
二、接口和抽象类
1.接口
是一种定义了一组方法和常量的规范,用于描述一个对象应该具备的行为方式。通常用于定义一组相似功能的操作,而不关心具体实现细节。它只关注对象如何和外界进行交互,而不关心对象的内部状态和具体实现方式。
在Java中,接口使用interface
关键字进行定义,并可以包含抽象方法、默认方法、静态方法和常量。实现类通过使用implements
关键字来实现接口,并需要实现接口定义的所有方法。一个类可以实现多个接口,从而同时具备多个接口定义的行为方式。
2.抽象类
描述一种类的概念和行为方式,但是不能直接被实例化。抽象类可以包含构造方法、属性、具体方法、抽象方法等成员,可以被子类继承和实现,从而实现代码复用和约束子类的行为方式。
在Java中,抽象类使用abstract
关键字进行定义,其中包含抽象方法和具体方法。抽象方法没有具体实现,只有方法签名,用于定义子类必须实现的方法;具体方法则有具体的实现,可以被继承或重写。子类通过使用extends
关键字继承抽象类,实现其中未实现的抽象方法,从而实现具体功能
两者的共同点和区别:
共同点:
① 都是抽象的数据类型,无法被直接实例化。
② 都可以包含抽象方法,需要被子类或实现类重写实现具体的功能。
③ 都支持多态,可以通过接口或抽象类的引用访问其实现类或子类的对象,实现动态绑定。
④ 都可以用于实现代码复用和约束子类或实现类的行为方式。
区别:
① 接口只能包含抽象方法、常量、默认方法和静态方法,不允许定义属性和具体方法;抽象类可以包含抽象和具体的方法、属性和构造方法。
② 一个类只能继承一个抽象类,但是可以实现多个接口;因此抽象类更适合用于描述一种类的共性,接口更适合用于描述一种行为的规范。
③ 实现接口的类必须实现接口中声明的所有方法;而抽象类可以有部分方法已经实现,子类只需要实现未实现的抽象方法即可。
④ 接口可以在运行时动态地传递给方法或对象,具有更强的灵活性;而抽象类在编译时就确定了其子类的行为方式,不容易发生意外错误。
三、深拷贝和浅拷贝
1.深拷贝
是将对象及其所有相关的对象完全复制一份,包括对象中的所有属性和关联的对象。深拷贝创建了一个独立的副本,修改副本不会影响原始对象。(类似于我们平时的复制操作)
2.浅拷贝
是将对象被拷贝的引用复制给新的对象,新对象和原始对象共享同一份数据,只复制对象本身及其基本类型的属性,而不复制关联的对象。在浅拷贝中,如果修改了新对象或原始对象中的共享属性,另一个对象也会受到影响。(类似于我们平时创建桌面快捷键)
3.引用拷贝
是指将对象的引用(内存地址)复制给另一个变量,使两个变量引用同一个对象。当修改其中一个变量所引用的对象时,另一个变量也会反映出相同的修改。这是因为两个变量实际上指向的是同一个对象。
需要注意的是,深拷贝和浅拷贝的概念适用于对象的拷贝操作,而引用拷贝是指将引用(内存地址)赋值给其他变量。在Java中,如果要进行对象的深拷贝或浅拷贝,可以通过重写对象的 clone()
方法或使用序列化和反序列化来实现。引用拷贝则是直接将对象的引用赋值给其他变量。
四、Object 类
Object类是Java中所有类的根类。它定义了一些通用的方法,可以在所有对象上使用。
1.Object类的主要方法
equals(Object obj)
: 用于判断当前对象是否与给定对象相等。
hashCode()
: 返回对象的哈希码值。
toString()
: 返回对象的字符串表示。通常会被子类重写以提供更有意义的字符串描述。
getClass()
: 返回对象的运行时类的Class对象。
clone()
: 创建并返回对象的浅拷贝(Shallow Copy)。
finalize()
: 当垃圾回收器确定没有对该对象的引用时,会调用该方法进行资源释放等清理操作。
notify()
和notifyAll()
: 用于线程间的通信,唤醒等待该对象监视器的线程。
wait()
: 使当前线程暂停执行,直到其他线程调用该对象的notify()
或notifyAll()
方法进行唤醒。
2.其中一些类需要注意的点
(1)"==" 和" equal "的区别
在Java中,"=="
和 "equals()"
是用于比较对象的两个不同的方法,区别有:
① "=="
操作符比较的是对象的引用:
A. 当使用 "=="
操作符比较基本类型时,比较的是它们的值是否相等
B. 当使用 "=="
操作符比较对象引用时,比较的是对象的内存地址是否相等
②" equals()"
方法比较的是对象的内容:
A.equals()
方法是Object类中定义的方法,默认情况下使用的是==
操作符进行比较。因此,如果没有在具体的类中重写equals()
方法,则默认比较的是对象的引用,效果与==
操作符相同。
B.通过在类中重写equals()
方法,可以根据对象的特定属性来比较两个对象的内容是否相等。一般而言,重写equals()
方法时通常也需要重写hashCode()
方法,以保持一致性。
(2)为什么重写 equals() 时必须重写 hashCode() 方法
这是为了保证一致性,在 Java 中,equals()
方法用于比较对象的内容是否相等,而 hashCode()
方法用于获取对象的哈希码,这在集合类(如 HashMap、HashSet 等)中经常用到。为了保持一致性,当重写 equals()
方法时,通常也需要重写 hashCode()
方法。
① 保证一致性: 哈希表(如 HashMap、HashSet 等)使用哈希码来分配对象的存储位置。当我们在集合中存储对象时,哈希表会根据 hashCode()
方法返回的哈希码来决定存储的位置。如果重写了 equals()
方法,但没有重写 hashCode()
方法,那么当对象内容相等但哈希码不同的情况下,会导致两者的不一致性,因为它们会被错误地存储到不同的位置,无法正确查找或者删除。
② 维护集合的性能: 当在哈希集合中存储大量对象时,哈希码的分布质量直接影响了集合的性能。如果在重写了 equals()
方法的同时不重写 hashCode()
方法,可能会导致哈希码分布不均匀,从而使得集合的性能下降,甚至出现退化情况。
因此,为了保持对象在集合中的正确存储和检索行为,以及维护哈希集合的性能,当我们重写了 equals()
方法时,最好也同时重写 hashCode()
方法,以确保两个方法的行为是一致的。
五、String类
1.String、StringBuffer和StringBuilder的区别
三者在Java中用于处理字符串的类,它们之间有以下区别:
可变性:String类是不可变的,意味着一旦创建了String对象,其值就不能被修改。对String对象进行拼接、替换等操作会创建新的String对象,原始的String对象不变。StringBuffer和StringBuilder类是可变的,可以动态修改字符串的内容。它们提供了一系列方法用于修改字符串,如追加(append)、插入(insert)、删除(delete)等操作。
线程安全性:String类是线程安全的,因为它的不可变性保证了多线程环境下的安全性。StringBuffer类是线程安全的,它的方法使用了synchronized修饰符来保证多线程环境下的同步访问。StringBuilder类是非线程安全的,它的方法没有使用synchronized修饰符,不保证多线程环境下的同步访问。
性能:String类由于不可变性的特性,在进行频繁修改操作时会产生大量的临时对象,对内存和性能有一定的开销。StringBuffer类适用于多线程环境下的字符串拼接和修改,虽然具有线程安全性,但在性能上相对较低。StringBuilder类适用于单线程环境下的字符串拼接和修改,它没有线程安全的开销,相对而言性能较高。
2.为什么String是不可变的
① 保存字符串的数组被 final
和private修饰,并且String
类没有提供修改这个字符串的方法。
② String
类被 final
修饰导致其不能被继承,进而避免了子类破坏 String
不可变。
3.字符串常量池的作用
字符串常量池(String Pool)是Java中的一种特殊的存储区域,用于存放字符串常量
字符串常量池的作用主要有以下几个方面:
① 节省内存空间:由于String类的不可变性质,相同的字符串常量只需在内存中存储一份即可。使用字符串常量池可以避免创建大量相同内容的字符串对象,从而节省内存空间。
② 提升性能:字符串常量池具有字符串缓存的效果。当创建一个String对象时,JVM首先检查字符串常量池中是否存在该字符串,如果存在,则直接返回常量池中的引用;如果不存在,则将该字符串添加到常量池中,并返回引用。这样可以减少对象的创建和垃圾回收的压力,提高程序的性能。
③ 字符串比较优化:由于常量池中的字符串是唯一的,因此可以使用"=="运算符进行字符串的比较,而无需通过equals()方法逐字符比较。这样可以提高字符串比较的效率。
需要注意的是,字符串常量池是在堆内存(Heap)中的,并且每个类加载器都维护着一个独立的字符串常量池。通过字面量方式创建的字符串常量会被自动放入常量池,而通过new关键字创建的字符串对象则不在常量池中。
String s1 = "Hello"; // 字符串常量池中创建一个"Hello"对象
String s2 = "Hello"; // 直接从常量池中获取"Hello"对象的引用,无需再次创建
String s3 = new String("Hello"); // 在堆内存中创建一个新的String对象,不在常量池中
System.out.println(s1 == s2); // 输出true,s1和s2引用同一个对象
System.out.println(s1 == s3); // 输出false,s1和s3引用不同的对象
六、异常
是指程序执行过程中遇到的错误或意外情况。当程序出现异常时,会打断正常的程序流程,并通过抛出异常(Throw Exception)的方式来通知调用者或上层代码。异常提供了一种机制,让我们能够优雅地处理错误情况,保证程序的稳定性和可靠性。
1.检查异常(Checked Exception)
这些异常是在编译阶段就可以被检测到的异常,需要在代码中显式处理或声明。如果没有处理或声明,编译器会报错。常见的编译时异常包括IOException、SQLException等。处理编译时异常的方式通常是使用try-catch语句块来捕获异常并进行处理。
try {
// 可能会抛出编译时异常的代码
} catch (异常类型1 e1) {
// 异常处理逻辑
} catch (异常类型2 e2) {
// 异常处理逻辑
} finally {
// 可选的finally块,无论是否发生异常,都会执行其中的代码
}
2.非检查异常(Unchecked Exception)
这些异常在编译阶段不会被检测到,只有在程序运行过程中才会抛出。常见的运行时异常包括NullPointerException、ArrayIndexOutOfBoundsException等。运行时异常可以不进行捕获或声明,但如果不处理,则会导致程序异常终止。
除了使用try-catch语句块来捕获和处理异常,还可以在方法声明中使用throws关键字将异常抛给上层调用者,让上层代码处理异常。这样的方法可以称为带有throws声明的方法。
public void someMethod() throws ExceptionType {
// 可能会抛出异常的代码
}
无论是捕获异常还是抛出异常,都可以在异常发生时执行特定的逻辑,如记录日志、回滚事务等。
3.常见的运行时异常
① NullPointerException(空指针异常):当代码尝试访问一个空对象的成员变量或调用空对象的方法时,会抛出NullPointerException异常。
② ArrayIndexOutOfBoundsException(数组下标越界异常):当代码中使用了非法的数组下标值进行访问数组元素时,会抛出ArrayIndexOutOfBoundsException异常。
③ ClassCastException(类转换异常):当尝试将一个对象转换为不兼容的类型时,会抛出ClassCastException异常。例如,将一个不是子类的对象强制转换为某个子类类型。
④ IllegalArgumentException(非法参数异常):当方法接收到一个不合法的参数时,会抛出IllegalArgumentException异常。通常在方法内部使用条件判断来验证参数的合法性。
⑤ ArithmeticException(算术异常):当出现除零操作或其他算术错误时,会抛出ArithmeticException异常。
⑥ UnsupportedOperationException(不支持的操作异常):当调用对象的某个方法,而该方法不支持当前操作时,会抛出UnsupportedOperationException异常。
⑦ ConcurrentModificationException(并发修改异常):当在迭代集合(如List、Set)的过程中,通过除迭代器本身的方式修改了集合的内容,会抛出ConcurrentModificationException异常。
⑧ RuntimeException(运行时异常的基类):RuntimeException是其他运行时异常的父类,它包含了常见的运行时异常,如IllegalStateException、NumberFormatException等。这些异常通常是由代码逻辑错误引起的。
3.Exception 和 Error 有什么区别
他们都是Throwable的子类,用来表示程序出现了不正常的情况
Exception:是程序正常运行过程中可能出现的异常情况,需要进行捕获或声明处理。
Error:是系统级别的错误或异常情况,无法通过代码进行恢复和处理。如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)
4.Throwable 类常用方法有哪些
Throwable是Java中所有错误或异常的超类,定义了异常和错误的基本属性和行为。其中,常用的方法包括:
① getMessage():获取异常或错误的详细描述信息,通常是由构造方法传入的字符串。
② toString():获取异常或错误的简短描述信息,通常包括异常或错误的类型和详细描述信息。
③ printStackTrace():在控制台打印异常或错误的堆栈跟踪信息,用于定位错误的位置和原因。
④ getCause():获取导致异常或错误的原因,通常返回一个Throwable对象。
⑤ getStackTrace():获取异常或错误的堆栈跟踪信息,返回一个数组,每个元素表示一条堆栈跟踪信息。
⑥ fillInStackTrace():重新生成堆栈跟踪信息,可以用于覆盖原有的堆栈跟踪信息,使其更准确或清晰。
这些方法都是从Throwable类继承而来,可以在子类中直接使用。在捕获和处理异常时,getMessage()和printStackTrace()方法可用于输出错误信息以及堆栈跟踪信息,方便开发人员进行问题定位和修复;getCause()方法则可用于查找并处理异常的原因,避免出现像“链式异常”的情况
5.try-catch-finally
try {
// 可能会抛出异常的代码块
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 类型的异常
} finally {
// 无论是否捕获到异常,都会执行的代码块(除了程序强制退出)
}
需要注意的是:不可以在 finally 语句块中使用 return
在finally代码块中使用return语句会导致异常被屏蔽,并且返回的结果会覆盖在try或catch代码块中的返回结果。这是由于finally代码块中的return语句会直接结束方法的执行,不再执行try或catch代码块中的return语句。
public static int test() {
try {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(test()); // 输出2,而不是1
}
6.异常处理需要注意的点
① 异常的捕获:在使用异常时,一定要为可能发生异常的代码加上try-catch语句或者throws声明。否则,如果程序出现异常,可能会导致程序崩溃或者无法正常运行。
② 异常的细化:在处理异常时,尽量使用细化的异常处理方式。即通过多个catch块来分别处理不同类型的异常,而不是只用一个catch块处理所有异常。这样可以使得代码更加清晰,并且更容易定位和解决问题。
③ 不要忽略异常:对于捕获到的异常,一定要进行适当的处理,不要简单地将其抛弃。对于不能立即处理的异常,可以通过日志记录等方式来进行记录,方便后续的排查和解决问题。
④ 异常捕获的粒度:在编写代码时,需要考虑异常捕获的粒度。过于宽泛的异常捕获会导致程序在出现异常时无法及时定位问题,过于细化的异常捕获可能会导致代码重复和冗余。需要根据实际情况来选择合适的异常捕获粒度。
⑤ 异常的抛出:在方法抛出异常时,应该提供有意义的异常信息,便于程序员识别和解决问题。