JavaTutorial02
面向对象特性
1.接口和抽象类有什么区别?
Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。
包含抽象方法的类叫做抽象类。
创建抽象类和抽象方法是有帮助的,这是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法。
一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。
接口的作用域中的所有东西都是public static final的,因为:
- public:因为接口的主要目的是定义公共的API,所以字段必须是公开的
- static:字段属于接口本身,而不是实现该接口的类的实例
- final:确保字段的值不能被修改,保持接口的不可变性
所以我们通常使用接口的字段作为常量定义,这也是为什么经常看到接口名以”Constants”结尾,如:
1 | interface DatabaseConstants { |
Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法。接口的基本概念仍然没变,介于类型之上、实现之下。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。
(1.概念和使用目的 2.如何使用 3.实践上如何区分)
接口和抽象类是Java面向对象设计的两个基础机制。
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何field都是隐含着public static final的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java标准类库中,定义了非常多的接口,比如java.util.List。
抽象类是不能实例化的类,用abstract关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和一般的Java类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java标准库中,比如collection框架,很多通用部分就被抽取成为抽象类,例如java.util.AbstractList。
Java类实现interface使用implements关键词,继承abstract class则是使用extends关键词
2.方法重写和重载有什么区别?
- 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
- 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
- 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
3.为什么需要内部类?什么是匿名内部类?
内部类的核心存在价值在于解决Java多重继承的限制问题。具体来说:
每个内部类都能独立地继承自一个接口或类,这使得外部类可以通过内部类来间接实现多个接口的功能
无论外部类是否已经继承了某个类或实现了某个接口,对内部类都没有影响
内部类提供了可以继承多个具体的或抽象的类的能力
实现接口更加灵活。当一个类需要实现某个接口,但又不想把所有的实现逻辑都放在主类中时,可以使用内部类来实现该接口,使得代码结构更加清晰。这种设计让多重继承的解决方案变得更加完整和灵活
匿名内部类是一种没有名字的内部类,它在声明的同时就进行实例化。主要用于:
- 实现接口或抽象类的一次性使用场景
- 适合创建只需要使用一次的类
例如:
1 | button.addActionListener(new ActionListener() { |
长得很像js里的调用函数(
4.静态内部类和非静态内部类有什么区别?
非静态内部类:
- 持有外部类的引用,可以访问外部类的所有成员(包括私有成员)
- 必须先创建外部类实例才能创建内部类实例
- 内存占用较大,因为每个实例都持有外部类的引用
静态内部类:
- 不持有外部类的引用,只能访问外部类的静态成员
- 可以直接创建实例,不需要外部类实例
- 内存占用更小,更加高效
5.静态内部类的使用场景是什么?
通俗来说,静态内部类就像是一个可以独立运作的模块,虽然它在代码上”住”在外部类里面,但它的运行不依赖外部类的实例,更像是一个”独立的房间”。
使用场景有:
当这个内部类不需要访问外部类的实例成员时:
1 | public class School { |
当内部类有独立的功能时:
1 | public class File { |
需要延迟加载某些功能时:
1 | public class Bank { |
语言特性
1.形参和实参的区别是什么?
形参: 就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
实参: 就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在Java方法调用的过程中,就是把实参传递给形参,形参的作用域在方法内部。
2.值传递和引用传递区别是什么?
值传递: 是指在调用方法时,将实际参数拷贝一份传递给方法,这样在方法中修改形式参数时,不会影响到实际参数。
引用传递: 也叫地址传递,是指在调用方法时,将实际参数的地址传递给方法 (如C++中的引用&) ,这样在方法中对形式参数的修改,将影响到实际参数。
也就是说值传递,传递的是副本。引用传递,传递的是实际内存地址。
3.Java是值传递还是引用传递?
值传递。
当传递基本数据类型,比如原始类型(int、long、char等)、包装类型(Integer、Long、String等),实参和形参都是存储在不同的栈帧内,修改形参的栈帧数据,不会影响实参的数据。
当传参的引用类型,形参和实参指向同一个地址的时候,修改形参地址的内容,会影响到实参。当形参和实参指向不同的地址的时候,修改形参地址的内容,并不会影响到实参。这里在参数传递的过程中,只是把实参的地址拷贝了一份传递给形参,只修改了参数地址里面的内容,并没有对形参本身进行修改。
4.final作用是什么?
我们可以将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不许修改的。
作用:
- 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成 final。
- final 变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。
5.final、finally、finalize 有什么不同?
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated。
6.static作用是什么?
1、static是一个修饰符,用于修饰成员。(成员变量,成员函数)static修饰的成员变量 称之为静态变量或类变量。
2、static修饰的成员被所有的对象共享。
3、static优先于对象存在,因为static的成员随着类的加载就已经存在。
4、static修饰的成员多了一种调用方式,可以直接被类名/静态成员所调用。
5、static修饰的数据是共享数据,对象中的存储的是特有的数据。
7.static和final区别是什么?
- 目的不同
- static: 用于共享(属于类)
- final: 用于不可变(防止修改)
- 使用时机不同
static:
- 需要在多个实例间共享数据
- 不需要访问实例数据的工具方法
final:
- 常量定义
- 防止方法被重写
- 防止类被继承
- 可以组合使用
1
2
3
4
5
6
7
8
9public class Constants {
// 静态常量:既共享又不可变
static final double PI = 3.14159;
// 静态最终方法:既可以直接调用,又不能被重写
static final void printVersion() {
System.out.println("v1.0");
}
}
使用建议:
- 工具类的方法通常是 static 的
- 配置常量通常是 static final 的
- 涉及安全的方法考虑用 final
- 不要过度使用 static,可能会增加类之间的耦合
数据类型
1.Java基本数据类型有几种?各占多少?
Java 语言提供了 8 种基本类型,大致分为 4 类
2.基础类型和包装类有什么区别?
(1)包装类型可以为 null,而基本类型不可以
别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行。比如:
1 | class Writer { |
那为什么 POJO 的属性必须要用包装类型呢?原因在于,数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱(将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值),就会抛出 NullPointerException 的异常。
(2)包装类型可用于泛型,而基本类型不可以
1 | List<int> list = new ArrayList<>(); // 提示 Syntax error, insert "Dimensions" to complete ReferenceType |
为什么呢?因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。
(3) 基本类型比包装类型更高效
基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。假如没有基本类型的话,对于数值这类经常使用到的数据来说,每次都要通过 new 一个包装类型就显得非常笨重。
3.自动装箱与拆箱了解吗?原理是什么?
引入包装类的目的就是:提供一种机制,使得基本数据类型可以与引用类型互相转换。
基本数据类型与包装类的转换被称为装箱和拆箱。
Java SE5 为了减少开发人员的工作,提供了自动装箱与自动拆箱的功能。
1 | Integer chenmo = 10; // 自动装箱 |
上面这段代码使用 JAD 反编译后的结果如下所示:
1 | Integer chenmo = Integer.valueOf(10); |
也就是说,自动装箱是通过 Integer.valueOf() 完成的;自动拆箱是通过 Integer.intValue() 完成的。
4.int和Integer区别是什么?
一个是基本数据类型,一个是包装类,是一个对象。
使用 Integer 的场景:
- 需要使用对象方法的场景
- 可能存在 null 值的情况
- 作为集合类的元素(集合不支持基本类型)
Integer方法:
1 | Integer num = 100; |
5.Integer缓存机制了解吗?
1 | Integer a = 127; //Integer.valueOf(127) |
当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。
一道面试题:
1 | // 1)基本类型和包装类型 True 自动拆箱 |
6.怎么避免浮点数精度丢失的问题?
为什么浮点数 float 或 double 运算的时候会有精度丢失的风险呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
因此,浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。
想要解决浮点数运算精度丢失这个问题,可以直接使用 BigDecimal 来定义浮点数的值,然后再进行浮点数的运算操作即可。
7.如何比较两个BigDecimal是否相等?
如何创建BigDecimal而不丢失精度?我们在使用 BigDecimal 时,为了防止精度丢失,推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建对象。
《阿里巴巴 Java 开发手册》中提到:
compareTo() 方法可以比较两个 BigDecimal 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。
1 | BigDecimal a = new BigDecimal("1"); |
其他常见BigDecimal方法:
- 加减乘除
1 | BigDecimal a = new BigDecimal("1.0"); |
这里需要注意的是,在我们使用 divide 方法的时候尽量使用 3 个参数版本,并且RoundingMode 不要选择 UNNECESSARY,否则很可能会遇到 ArithmeticException(无法除尽出现无限循环小数的时候),其中 scale 表示要保留几位小数,roundingMode 代表保留规则。
1 | public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { |
- 保留几位小数
1 | BigDecimal m = new BigDecimal("1.255433"); |
常见类
1.Object 类的常见方法有哪些?
2.==和 equals()的区别是什么?
现在,让我们总结一下 == 操作符和 equals() 方法的区别:
- == 操作符比较对象的引用,判断是否为同一对象。
- equals() 是 Object 类中定义的方法,用于比较对象的内容,但默认情况下与 == 的行为相同(比较引用)。
- 一些类会覆盖 equals() 方法以便在内容上进行比较,例如 String、Integer、Double 等。
当你需要比较对象的内容时,务必使用适当的方法。同时要注意处理参数为 null 的情况,以及在自定义类中正确地覆盖 equals() 方法,以便在语义上正确地比较对象内容。
String类源码:
1 | public boolean equals(Object anObject) { |
User类重写equals:
1 | import java.util.Objects; |
3.为什么要有 hashCode?
主要原因:性能优化
在哈希表(如HashMap、HashSet)这类数据结构中,如果仅使用equals比较对象是否相等,需要逐个比较,时间复杂度为O(n);
而使用hashCode可以先计算对象的哈希值,快速定位到存储桶(bucket),将时间复杂度优化到接近O(1)。
只有在发生哈希碰撞时才需要使用equals进行详细比较
4.hashCode和equals区别是什么?
hashCode和equals方法的区别可以从以下几个方面来说明:
- 设计目的不同:
- equals: 用于判断两个对象的内容是否相等
- hashCode: 用于获取对象的哈希码,主要用于在散列数据结构中快速查找对象
- 关系约定:
- 如果两个对象equals相等,它们的hashCode必须相等
- 如果两个对象hashCode相等,它们的equals不一定相等(可能发生哈希碰撞)
- 如果两个对象hashCode不相等,它们的equals一定不相等
hashCode计算相对简单,性能开销小;equals通常需要比较对象的多个属性,性能开销较大
两函数的返回值必须是确定值
在集合中的应用:
1 | HashMap<Person, String> map = new HashMap<>(); |
5.为什么重写 equals()时必须重写 hashCode()方法?
当我们对比两个对象是否相等时,我们就可以先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。这样就大大的提升了对象比较的效率,这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式,来确认两个对象是否相等的原因。
那为什么不直接使用 hashCode 就确定两个对象是否相等呢?这是因为不同对象的 hashCode 可能相同;但 hashCode 不同的对象一定不相等,所以使用 hashCode 可以起到快速初次判断对象是否相等的作用。
为什么要同时重写呢?是因为如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象。
但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。
6.浅拷贝和深拷贝有什么区别?
1 | class Classroom { |
简单来说:
- 浅拷贝:复制对象本身,但里面的对象还是引用原来的
- 深拷贝:复制对象本身,同时也复制对象内部的所有对象,形成完全独立的新对象
7.深拷贝有几种实现方式?
构造函数
我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。
1 | // 调用构造函数时进行深拷贝 |
重载clone()方法
Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。
1 | public class User implements Cloneable { |
这就像是一个两步操作:
- 先复制”框架”(User对象本身)
- 再复制”内容物”(User对象中的引用类型属性)
1 | 步骤1 (super.clone()): |
另外,虽然String类也是引用类型,但是由于其特殊的不可变特性,因此也不用new,java会帮我们自动创建新的对象。接下来的两个问题会有提到。
Serializable接口序列化
Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。
1 |
|
8.String、StringBuffer、StringBuilder的区别?
参考链接:https://dunwu.github.io/javacore/pages/bc583c/#string-%E7%9A%84%E4%B8%8D%E5%8F%AF%E5%8F%98%E6%80%A7
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 是一个线程安全的可修改字符序列。StringBuffer 的线程安全是通过在各种修改数据的方法上用 synchronized 关键字修饰实现的。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。
除非有线程安全的需要,不然一般都使用 StringBuilder
使用示例:
1 | public class StringBuilderDemo { |
9.Java的 String 类为什么不可变?
1.String 类被 final 修饰,是不可变类
2.String 类的数据存储在 char[]数组中,这个数组被 final 修饰,表示 String 对象不可被更改
为什么 Java 要这样设计?
(1)保证 String 对象安全性。避免 String 被篡改。
(2)保证 hash 值不会频繁变更。
(3)可以实现字符串常量池。





