文章目录
- 1.Java语言有哪些特点?
- 2.面向对象和面向过程的区别?
- 3.八种数据类型大小、封装类
- 4.instanceof关键字的作用
- 5.Java自动装箱与拆箱
- 6. 重载与重写的区别
- 7. == 与 equals的区别
- 8.hashCode的作用
- 9. JVM vs JDK vs JRE
- 10.什么是字节码?采用字节码的好处是什么?
- 11.Object类有哪些常见的方法?
- 12.Java创建对象有几种方法?
- 13.获取类Class对象的方式有哪些?
- 14.什么是泛型?泛型有什么作用?
- 15.序列化与反序列化
- 16. 3*0.1 == 0.3返回值是什么?
- 17. a=a+b与a+=b有什么区别吗?
- 18.try-catch-finally如何正确使用?
- 19.深拷贝和浅拷贝区别了解吗?
- 20.抽象类与接口的区别
1.Java语言有哪些特点?
1.简单易学、有丰富的类库
2.面向对象(Java最重要的特性、让程序耦合度更低、内聚性更高)
3.与平台无关性(JVM是Java跨平台使用的根本)
4.可靠安全
5.支持多线程
2.面向对象和面向过程的区别?
面向过程:== 是分析解决问题的步骤、然后是由函数将这些步骤一步步的实现,然后在使用时一一调用即可。性能较高,单片机、嵌入式开发一般采用面向过程开发
面向对象: 是把构成问题的事物分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决问题的过程中所发生的行为。面向对象有:封装、继承、多态的特性,所以易维护、易服用、易扩展。可以设计出低耦合的系统。性能上来说比面向过程要低
3.八种数据类型大小、封装类
基本类型 | 大小(字节) | 默认值 | 封装类 |
---|---|---|---|
byte | 1 | (byte)0 | Byte |
short | 2 | (short)0 | Short |
int | 4 | 0 | Integerr |
long | 8 | 0L | Long |
float | 4 | 0.0f | Float |
double | 8 | 0.0d | Double |
boolean | - | false | Boolean |
char | 2 | \u0000(null) | Character |
注:
1.int是基本数据类型,Integer是int的包装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分0和null和情况。一旦Java看到null,就知道该引用没有指向某个对象,在使用前必须指定一个对象,否则会报错
2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟空间后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以一个数组所做的修改在另一个数组中也看得见
4.instanceof关键字的作用
instanceof是Java中的一个双目运算符,用来判断一个对象是否为一个类的实例,用法为:
boolean result = obj instanceof Class;
其中obj为一个对象,Class表示一个类或者接口,当obj为Class的对象或者是直接或间接子类,或者是其接口的实现类,都返回true,否则返回false.
注意:编译器会检查obj是否能转换成右边的Class类型,如果不能转换直接报错,如果不能确定类型,则通过编译,具体看运行实际
int i = 1;
System.out.println(i instanceof Integer); //编译失败 i必须为引用类型
System.out.println(i instanceof Object); //编译失败
Integer integer = new Integer(10);
System.out.println(integer instanceof Integer); // true
// JavaSE规范,使用instanceof: 如果obj为null, 那么返回false
System.out.println(null instanceof Object);
5.Java自动装箱与拆箱
装箱:== 装箱就是自动将基本数据类型转换为包装类型(int -> Integer);调用Integer的valueOf(int)方法
拆箱: 拆箱就是自动将包装类型转换为基本数据类型(Integer -> int);调用Integer的intValue方法
JDK5之前,想要得到对象必须进行以下操作:
Integer i = new Integer(5);
JDK5开始提供了自动装箱的特性:
Integer i = 5;
经典面试题1:
以下代码会输出什么?
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
为什么会出现上述结果,表名i1和i2指向的是同一个对象,而i3和i4指向的是不同对象,下面是Integer的valueOf方法的具体实现:
IntegerCache的具体实现:
我们可以发现,在通过valueOf方法创建Integer对象时,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象引用;否则创建一个新的Integer对象
经典面试题2:
以下代码输出什么?
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
}
原因:在某个范围内整型数值个数是有限的,而浮点数不是
6. 重载与重写的区别
重写(Override): 从字面看,重写就是重新写一遍的意思。其实就是在子类把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但子类并不想原封不动的继承父类的某个方法,所以在方法名,参数列表,返回类型(除过子类的方法返回值是父类方法中返回值的子类时)都相同的情况下,对方法体进行修改或重写,这就是重写。但需要注意子类函数的访问修饰权限不能低于父类
public class Father {
public void sayHello() {
System.out.println("hello father");
}
}
class Son extends Father {
@Override
public void sayHello() {
System.out.println("hello son");
}
}
总结:1. 重写发生在父类与子类之间
2. 方法名、参数列表、返回类型(除过子类中的方法返回类型是父类返回类型的子类)必须相同
3. 访问修饰权限一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.重写方法一定不能抛出新的检查异常或者比重写方法更加宽泛的检查异常
重载(Overload): 在一个类中,同名方法如果有不同参数类型(参数类型不同、参数个数不同、参数顺序不同)则视为重载,对返回值没有要求,可以相同可以不同,不能通过返回类型来判断是否重载
public class Father {
public void sayHello() {
System.out.println("hello father");
}
public void sayHello(String name) {
System.out.println("hello " + name);
}
}
总结:1.重载Overload是一个类中多态的一种表现
2.重载要求同名方法的参数列表不同(参数类型、参数格式、参数顺序)
3.重载时,返回类型可以相同也可以不同,返回类型无法作为重载函数的区分标准
7. == 与 equals的区别
== 对于基本类型和引用类型的作用效果是不同的
1.对于基本数据类型来说,== 比较的是值
2.对于引用数据类型来说,== 比较的是对象的内存地址
因为Java只有值传递,所以对于 == 来说,不管是比较基本数据类型还是引用数据类型,本质上比较的都是值,只不过引用类型变量存的值是对象的地址
equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因为所有类都有equals()方法
// Object 类 中的equals()方法
public boolean equals(Object obj) {
return (this == obj);
}
使用equals()方法分为以下两种情况:
1.类没有重写了equals()方法: 通过equals()比较该类的两个对象时,等价于通过 == 比较这两个对象,使用的默认是Object类中的equals()方法
2.类重写了equals()方法: 一般我们都重写equals()方法类比较两个对象中的属性是否相等,如果相等返回true(认为这两个对象相等)
public static void main(String[] args) {
String a = new String("abcd");
String b = new String("abcd");
String aa = "abcd";
String bb = "abcd";
System.out.println(aa == bb);
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(42 == 42.0);
}
String中的equals()方法是重写过的,因为Object的equals()方法比较的是对象的内存地址,而String的equals()方法比较的是对象的值.
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的要和要创建的值相同的对象,如果有就直接赋值给当前引用,如果没有就在常量池创建一个String对象
String类的equals()方法:
8.hashCode的作用
hashCode()的作用是获取哈希码(int整数),也成为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置
hashCode()是定义在JDK的Object类中的,意味着Java中的任何类都包含着hashCode()方法。需要注意的是Object的hashCode()方法是本地方法,也就是使用C语言或C++实现的。
⚠️ 注意:该方法在 Oracle OpenJDK8 中默认是 “使用线程局部状态来实现 Marsaglia’s xor-shift 随机数生成”, 并不是 “地址” 或者 “地址转换而来”, 不同 JDK/VM 可能不同在 Oracle OpenJDK8 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。
public native int hashCode();
散列表存储到是键值对(key-value),它最大的特点是能根据键值快速检索出对应的值,其中就利用了散列值。
为什么要有hashCode?
当我们把对象加入到HashSet时,HashSet会先计算对象的hashCode值来判断加入的位置,同时也会与其他已经加入对象的hashCode值做比较,如果没有没有相符合的hashCode,就会认为该对象没有重复出现。如果发现有相同的hashCode,就会调用equals()方法来检查hashCode相等的对象是否真正相同,如果相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置,这样我们就大大减少了equals()的次数,大大提高了执行速度。
为什么JDK要同时提供这两个方法呢?
这是因为一些容器(HashMap、HashSet)中,有了hashCode()之后,判断元素是否在容器中效率会更高。
同样的hashCode有多个对象时,会使用equals()判断是否真的相同。也可以理解为hashCode帮助我们大大缩小了查找成本
两个对象的hashCode值相等并不代表两个对象就相等
为什么两个相同的hashCode值,它们也不一定是相等的?
因为hashCode()使用的哈希算法也许会让多个对象传回相同的哈希值,越糟糕的哈希算法越容易碰撞。
可以总结为以下三点:
1、如果两个对象的hashCode相等,两个对象不一定相等(哈希碰撞)
2、如果两个对象的hashCode相等并且equals()返回true,则认为两个对象相等
3、如果两个对象的hashCode不相等,我们直接认为两个对象不相等
为什么重写equals()时必须重写hashCode()方法?
因为两个相等对象的hashCode必须是相等的。也就是如果equals()方法判断两个对象是相等的,那么像个对象hashCode也要相等。
如果重写euqals()没有重写hashCode()方法的话,就会导致euqals()方法判断相等的两个对象,hashCode值却不相等
9. JVM vs JDK vs JRE
JVM: Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同的系统的特定实现(Windows、Linux、macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同的系统的JVM实现Java语言一次编译,随处可以运行的关键所在。
JVM并不是只有一种,只要满足JVM规范即可。我们平时接触到的HotSpot VM 仅仅是JVM规范的一种实现而已。
JDK(Java Development Kit): 它是功能齐全的Java SDK,提供给开发者使用的,能够创建和编译Java程序,它包含JRE,同时包含了编译Java源代码的编译器javac以及一些其他工具javadoc(文档注释工具)、jdb(调试器)、jconsole(基于JMX的可视化监控工具)、javap(反编译工具)等等。
JRE(Java Runtime Environment): Java运行时环境。它是运行已编译Java程序所需的所有内容的集合,主要包括Java虚拟机JVM、Java基础类库(Class Library)
10.什么是字节码?采用字节码的好处是什么?
JVM可以理解的代码就叫做字节码(.class文件),不面向任何特定的处理器,只面向虚拟机。在一定程度上解决了传统解释性语言执行效率低的问题,同时又保留了解释型语言可移植的特点
我们需要注意的是 .class -> 机器码这一步。JVM类加载器首先加载字节码文件,然后解释器逐行解释执行,这样的方式执行速度相对较慢,而且有些方法和代码块是经常需要被调用的(热点代码),所以引入了JIT(just-in-time compilation) 编译器,而JIT属于运行时编译,当JIT完成第一次编译后,会将机器码保存下来,下次直接使用,这也是我们为什么说Java是编译与解释共存的语言
HotSpot根据二八定律,消耗大部分系统资源的只有那一小部分的代码,也就是JIT所需要编译的部分,JVM会根据每次被执行的情况做出优化。JDK9引入AOT(Ahead of Time Compliation)直接将字节码编译成机器码,编码了JIT预热的消耗
11.Object类有哪些常见的方法?
// native 方法,用于创建并返回当前对象的一份拷贝。
protected native Object clone() throws CloneNotSupportedException
保护方法,实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常,深拷贝也需要实现Cloneable接口,成员变量为引用类型的也需要实现Cloneable接口,然后重写clone方法
// 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
public boolean equals(Object obj)
该方法使用频率非常高,equals和 == 不一样的,在Object中是一样的,子类一般要重写该方法
// native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public native int hashCode()
该方法用户哈希查找,重写了equals方法一般都要重写hashCode方法,这个方法在一些具有哈希功能的Collection中用到。
o1.equals(o2) == true可以推出: o1.hashCode() == o2.hashCode(),但是hashCode相等不一定就满足equals
JDK1.6、1.7: 默认是返回随机数
JDK1.8: 默认是通过和当前线程有关的一个随机数 + 三个确定值,运用Marsaglia’s xorshift scheme 随机数算法得到的一个随机数。
// 实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }
该方法和垃圾收集器有关系,判断一个对象是否可以被收回的最后一步就是判断是否重写了此方法
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。nanos 参数,这个参数表示额外时间
*/
public final void wait(long timeout, int nanos) throws InterruptedException
配合synchronized使用。wait方法就是使当前线程等待该对象的锁,前提是当前线程具有该对象的锁
调用该方法后,当前线程进入睡眠状态,直到一下事件发生:
- 其他线程调用了该对象的notify方法
- 其他线程调用了该对象的notifyAll方法
- 其他线程调用了interrupt中断该线程
- timeout到了
此时线程就可以被调度,如果是被中断抛出一个InterruptedException异常
// 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString()
该方法我们需要在子类重写,一般是用来打印对象相关信息
// native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public final native Class<?> getClass()
获取类对象
public final native void notify()
public final native void notifyAll()
notify方法: 配合synchronized使用,该方法唤醒该对象上等待队列上的某个线程
notifyAll方法: 配合synchronized使用,该方法用于唤醒该对象上等待队列上的所有线程
12.Java创建对象有几种方法?
1.使用new关键字
Student student = new Student();
2.使用反射方式创建: 使用newInstance(),但是需要处理两个异常 InstantiationException、illegalAccessException:
Student student = Student.Class.newInstance();
Object object = (Object)Class.forName("Java.lang.Object").newInstance();
3.使用clone方法: 使用clone进行克隆对象
4.使用反序列化创建对象: 调用ObjectInputStream类的readObject()方法。
我们在反序列化对象时,JVM会给我们创建一个单独的对象。JVM创建对象不会调用任何构造方法。一个对象实现了Serializable接口,就可以将对象写入到文件中,然后通过读取文件来创建对象
13.获取类Class对象的方式有哪些?
大家需要搞清楚类对象和实例对象的区别
1.通过对象的getClass()方法获取: 这个getClass是Object类的方法
Student student = new Student();
// clazz 就是Student的类对象
Class<?> clazz = student.getClass();
2.通过类的静态成员表示: 每个类都有隐含的静态成员变量class
// clazz 就是Student的类对象
Class<?> clazz = Student.class;
3.通过Class类的静态方法forName()方法获取
Class<?> clazz = Class.forName("com.zd.Student");
14.什么是泛型?泛型有什么作用?
Java泛型是JDK 5 中引入的一个新特性。使用泛型参数可以增强代码的可读性和稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定输入对象的类型。比如:
List<Integer> list = new ArrayList<>();
这行代码指明了该List对象只能传入Integer类型,如果传入其他类型的对象就会报错
ArrayList<E> extends AbstractList<E>
原生的List 返回类型是Object,需要我们手动转换类型才能正常使用,我们使用泛型后编译器帮助我们自动进行转换
泛型的使用方法有哪几种?
泛型一般的用法有三种:泛型类、泛型接口、泛型方法
1.泛型类:
public class MyList<T> {
private T val;
public MyList(T val) {
this.val = val;
}
public T getVal() {
return val;
}
}
如何实例化:
MyList<Integer> list = new MyList<Integer>(10);
2.泛型接口:
public interface MyList<T> {
public T func();
}
实现泛型接口,不指定类型:
class MyList1<T> implements MyList<T> {
@Override
public T func() {
return null;
}
}
实现泛型接口,指定类型:
class MyList2<T> implements MyList<String> {
@Override
public String func() {
return "MyList2";
}
}
3.泛型方法:
普通方法:
public <泛型形参> T findMax(T[] arr)
class MyArray {
public <T extends Comparable<T>> T findMax(T[] arr) {
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(max) > 0) {
max = arr[i];
}
}
return max;
}
}
public static void main(String[] args) {
Integer[] arr = {1,5,4,7,9,2};
MyArray myArray = new MyArray();
System.out.println(myArray.findMax(arr));
}
静态方法:
我们直接给方法加上static时,系统报错了,因为static方法是在类加载指定,擦除机制是在编译时检查和强制转换,在类加载时,并不知道是什么类型,所以不能直接加static。
public static<泛型形参> T findMax(T[] arr)
class MyArray {
public static<T extends Comparable<T>> T findMax(T[] arr) {
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(max) > 0) {
max = arr[i];
}
}
return max;
}
}
public static void main(String[] args) {
Integer[] arr = {1,5,4,7,9,2};
Integer max = MyArray.findMax(arr);
System.out.println(max);
}
这样我们就可以不依赖对象求出最大值了,那类型实参在那里传?
一般在引用点的后面,一般可以省略。
15.序列化与反序列化
什么是序列号化?什么是反序列化?
序列化:将数据结构或对象转成二进制字节流的过程
反序列化:将序列化所生成的二进制字节流转换成数据结构或者对象的过程
在Java这种面向对象编程语言来说,序列化的都是对象,也就是实例化后的类。
序列化的主要目的是通过网络传输对象,或者是将对象存储到文件系统、数据库、内存中
序列化协议位于TCP/IP模型中的那一层?
我们可以看到OSI七层协议模型中,表现层做的事情就是将用户数据进行转换为二进制流,反过来就是将二进制流转换为应用层的用户数据。对应的就是我们的序列化与反序列化。因此序列化协议属于TCP/IP协议应用层的一部分
如果有字段不想序列化怎么办?
如果有字段不想序列化,可以使用transient 关键字修饰.
transient的作用:阻止实例中使用该关键字修饰的变量序列化;当对象被反序列化时,被该关键字修饰的变量不会被持久化和恢复
注意:
1.transient只能修饰变量,不能修饰类和方法
2.transient修饰的变量,在反序列化后变量值会被设置成类型的默认值。例如修饰的int,反序列化后为0
3.static变量因为不属于对象,所以无论是否被transient修饰,均不会被序列化
为什么不推荐使用JDK自带的序列化?
1.性能差: 相比于其他序列化框架性能更差,主要原因是序列化之后字节数组体积较大,传输成本加大
2.不支持跨语言调用: 如果调用其他语言开发的服务不支持
3.存在安全问题: 序列化和反序列化本身不存在问题。但如果反序列化的数据被人控制,会通过伪造恶意输入,使得反序列化产生非预期的对象
16. 3*0.1 == 0.3返回值是什么?
public static void main(String[] args) {
System.out.println(3 * 0.1 == 0.3);
}
因为有些浮点数不能完全精确的表示出来
17. a=a+b与a+=b有什么区别吗?
+= 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为结果的类型,而a=a+b则不会自动进行类型转换
public static void main(String[] args) {
byte a = 127;
byte b = 127;
b = a + b;
}
public static void main(String[] args) {
byte a = 127;
byte b = 127;
b += a;
}
以下代码是否有错,有的话如何改?
short s = 1;
s = s + 1;
有错误,short类型在进行运算时会提升为int类型,也就是s+1的运算结果为int类型,而s的类型为short类型,所以编译器会报错
正确写法:
short s = 1;
s += 1;
+= 操作符会对右边的表达式结果强转匹配左边的数据类型
18.try-catch-finally如何正确使用?
try块: 用户捕获异常。后面可以接0或多个catch块,如果没有catch则必须跟一个finally块
catch块: 用于处理try块捕获到的异常
finally块: 无论是否捕获或处理异常,finally块里的代码都会执行
public static void main(String[] args) {
try {
System.out.println("try");
System.out.println(10 / 0);
} catch (Exception e) {
System.out.println("catch");
} finally {
System.out.println("finally");
}
}
注意:
不要再finally里面使用return语句,因为如果当try和finally里面都有return语句,会忽略try中的。我们try语句中的return会暂时存在一个本地变量,当执行fianlly语句中的return后,这个变量就变为fianlly语句中的return返回值
public static void main(String[] args) {
System.out.println(func());
}
public static int func() {
try {
return 1;
} finally {
return 2;
}
}
finally中代码一定会执行吗?
不一定。在一些情况下,finally中的代码不会被执行,例如:在finally执行之前虚拟机被终止
public static void main(String[] args) {
try {
System.out.println("try");
System.out.println(10 / 0);
} catch (Exception e) {
System.out.println("catch");
System.exit(1);
} finally {
System.out.println("finally");
}
}
还有以下情况,finally块代码不执行:
1.关闭CPU
2.线程死亡
try-with-resources如何使用?
1.适用范围:必须实现java.lang.AutoCloseable 或者 java.io.Closeable 的对象
2.关闭资源和finally块的执行顺序:任何catch 或 finally 块在声明的资源关闭后运行
// try - catch - finally
public static void main(String[] args) {
Scanner scan = null;
try {
scan = new Scanner(System.in);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(scan != null) {
scan.close();
}
}
}
这种方法有什么弊端呢?当程序出现严重错误时,finally将不会执行,资源就无法得到释放
// try - with - resources
public static void main(String[] args) {
try (Scanner scan = new Scanner(System.in)){
System.out.println(scan.next());
} catch (Exception e) {
e.printStackTrace();
}
}
当我们在程序需要使用必须关闭的资源时,我们应该优先使用 try - with - resources 而不是 try - finally,代码更简短,更清晰,帮助我们关闭资源
如何正确使用异常?
- 抛出的异常信息要有意义
- 使用日志打印异常就不要在抛异常了(两者存在一个即可)
- 不要将异常定义为静态变量,会导致异常栈信息错乱。我们需要手动new一个异常对象抛出
- 抛出的异常建议更加具体,而不是父类Exception
19.深拷贝和浅拷贝区别了解吗?
- 浅拷贝(Shallow Copy):浅拷贝创建一个新对象,新对象的属性与原对象相同。如果属性是基本数据类型,则复制其值;如果属性是引用类型,则复制其引用(指针),即新对象与原对象引用同一个对象。因此,在浅拷贝中,修改新对象的引用类型属性会影响到原对象。
- 深拷贝(Deep Copy):深拷贝创建一个全新的对象,新对象的属性与原对象相同,但是它们指向的是不同的对象。在深拷贝中,无论属性是基本数据类型还是引用类型,都会被复制一份,即创建了一个独立的副本。因此,在深拷贝中,修改新对象的引用类型属性不会影响到原对象。
浅拷贝:
public class Student implements Cloneable{
private String name;
@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
}
class Person implements Cloneable {
private Student student;
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
浅拷贝的使用很简单,实现Cloneable接口,重写clone()方法,直接调用Object的clone()方法
我们可以发现浅拷贝后,引用属性指向的是同一个
深拷贝:
我们需要对Person中的引用属性也进行拷贝
class Person implements Cloneable {
private Student student;
public Person(Student student) {
this.student = student;
}
@Override
protected Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.student = person.student.clone();
return person;
}
}
我们可以发现深拷贝后,引用属性指向的不同的
引用拷贝:
引用拷贝就是两个不同的引用指向同一对象
20.抽象类与接口的区别
抽象类(Abstract Class)和接口(Interface)是面向对象编程中的两个重要概念,它们在定义和使用上有以下区别:
- 定义形式:抽象类使用abstract关键字定义,可以包含抽象方法和具体方法;接口使用interface关键字定义,只能包含抽象方法和常量字段。
- 实现方式:一个类可以继承(extends)一个抽象类,通过关键字implements实现多个接口。如果一个类继承了一个抽象类,则其必须实现抽象类中的所有抽象方法或将自身也声明为抽象类;而接口可以被多个类同时实现。
- 默认实现:抽象类可以包含具体方法的实现,子类可以选择性地覆盖这些方法;接口只能包含抽象方法,没有方法的实现。在Java 8之后,接口可以通过默认方法(default method)提供具体的方法实现,但默认方法必须使用default关键字进行修饰。
- 单继承与多实现:一个类只能继承一个抽象类,但可以实现多个接口。这是由于Java中的单继承性质,即一个类只能有一个直接父类;而接口可以被多个类实现,从而实现多态的特性。
- 设计目的:抽象类用于表示一种"is-a"关系,即子类是父类的一种具体类型;接口用于表示一种"has-a"关系,即类具有某些能力或功能。
总的来说,抽象类更适合用于代码复用和继承关系的建模,而接口更适合用于定义规范、实现多态和实现类的解耦。选择抽象类还是接口要根据具体的需求和设计目的来决定。