面试集锦(一)Java基础

1. 基础

1.1 ==,equals和hashCode

概念:

  • == : 该操作符生成的是一个boolean结果,它计算的是操作数的值之间的关系;
  • equals : Object 的 实例方法,比较两个对象的content是否相同。
  • hashCode : Object 的native方法 , 获取对象的哈希值,用于确定该对象在哈希表中的索引位置,它实际上是一个int型整数。

关系操作符 ==:

  • 若操作数的类型是基本数据类型,变量直接存储的是“值”。因此,在使用关系操作符 == 来进行比较时,比较的就是“值”本身;
  • 若操作数的类型是引用数据类型,则该关系操作符判断的是左右两边操作数的内存地址是否相同。也就是说,若此时返回true,则该操作符作用的一定是同一个对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KrHAqtRr-1602775315902)(C:\Users\vocyd\AppData\Roaming\Typora\typora-user-images\image-20200911091450815.png)]


equals方法:

  • 本意比较两个对象的 content 是否相同
  • 必要的时候,我们需要重写该方法,避免违背本意,且要遵循上述原则。

在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。通过new初始化相同值的两个String,equals的结果相同,因为String重写了该方法。

使用equals方法,内部实现分为三个步骤:

  • 先比较引用是否相同(是否为同一对象);
  • 再判断类型是否一致(是否为同一类型);
  • 最后比较内容是否一致

Java 中所有内置的类的 equals 方法的实现步骤均是如此,特别是诸如 Integer,Double 等包装器类。

equals 重写原则:

对象内容的比较才是设计equals()的真正目的,Java语言对equals()的要求如下,这些要求是重写该方法时必须遵循的:

  • 对称性: 如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true” ;
  • 自反性: x.equals(x)必须返回是“true” ;
  • 类推性: 如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true” ;
  • 一致性: 如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true” ;
  • 任何情况下,x.equals(null)(应使用关系比较符 ==),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。

hashCode方法:

Object 的 hashcode 方法是本地方法,也就是用 C 或 C++ 实现的,该方法直接返回对象的内存地址。如果没有重写hashCode方法,则任何对象的hashCode值都不相等。

首先了解hash表:

Hash 就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出(int),该输出就是散列值。这种转换是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。简单的说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

常用实现方法:拉链法 – 链表的数组。

数组的每个成员是一个链表。该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。

要点:

  • hash函数选择:针对字符串,整数,排列,具体相应的hash方法;
  • 碰撞处理:一种是open hashing,也称为拉链法:另一种就是closed hashing,也称开地址法,opened addressing。

hashCode:

在 Java 中,由 Object 类定义的 hashCode 方法会针对不同的对象返回不同的整数。(这是通过将该对象的内部地址转换成一个整数来实现的,但是 Java编程语言不需要这种实现技巧)。

hashCode 的常规协定是:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法 不要求 一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

对于判断集合元素重复问题,用到了Object.equals 方法;但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。 于是,Java采用了哈希表的原理。 这样,我们对每个要存入集合的元素使用哈希算法算出一个值,然后根据该值计算出元素应该在数组的位置。所以,当集合要添加新的元素时,可分为两个步骤:

  1. 先调用这个元素的 hashCode 方法,然后根据所得到的值计算出元素应该在数组的位置。如果这个位置上没有元素,那么直接将它存储在这个位置上;
  2. 如果这个位置上已经有元素了,那么调用它的equals方法与新元素进行比较:相同的话就不存了,否则,将其存在这个位置对应的链表中(Java 中 HashSet, HashMap 和 Hashtable的实现总将元素放到链表的表头)。

equals与hashCode:

  • 原则 1 : 如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必须相等 ;
  • 原则 2 : 如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
  • 原则 3 : 如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;
  • 原则 4 : 一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
  • 原则 5 : 当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。
  • hashcode是系统用来快速检索对象而使用;
  • equals方法本意是用来判断引用的对象是否一致;
  • 重写equals方法和hashcode方法时,equals方法中用到的成员变量也必定会在hashcode方法中用到,只不过前者作为比较项,后者作为生成摘要的信息项,本质上所用到的数据是一样的,从而保证二者的一致性。

HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals()。若相等则认为他们是相等的。若equals()不相等则认为他们不相等。
如果只重写equals没有重写hashCode(),就会导致相同的key值也被hashcode认为是不同的key值(因为没有重写hashCode(),则任何对象的hashCode()值都不相等),就会在HashMap中存储相同的key值(Map中key值不能相同),这就不符合条件了。


1.2 操作字符串的类

String、StringBuffer、StringBuilder

从类的继承关系上来开的话,String和StringBuffer、StringBuilder是没有任何关系的,但是StringBuffer和StringBuilder的继承关系(AbstractStringBuilder)是一样的。

三个类存储的本质都是一个char类型数组,不同的是String类型的数组长度是3,而另外两个数组的长度都是19且默认值为0。String类是不可变的字符串,而另外两个类都是可以对字符串进行追加的。

追加的方法实际上都是调用父类的追加方法,判断当前char[]数组能不能装下这个追加之后的字符串,能装下的话什么都不做,不能装下的话会使用Arrays.copyOf()方法将现有的char[]数组赋值到新的char数组中,新数组的长度使用newCapacity()方法获得。

  • String类型的字符串是不可变的,对String类型的字符床做修改操作都是相当于重新创建对象;它主要的三个成员变量 char value[], int offset, int count均是private,final的,并且没有对应的 getter/setter;
  • StringBuffer中的方法大部分都使用synchronized关键字修饰,所以StringBuffer是线程安全的;
  • StringBuilder因为没有使用使用synchronized关键字修饰,所以性能更高,在单线程环境下会选择使用StringBuilder,多线程环境下使用StringBuffer。

字符串反转:添加到StringBuilder中,调用reverse方法。


1.3 Java特性

面向对象的特征:

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。如把成员变量定义为private。


继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。子类拥有父类非 private 的属性、方法。要实现继承,可以通过 继承和组合 来实现。

继承提高了类之间的耦合性(继承的缺点:耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。


多态

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单说就是一句话:允许将子类类型的指针赋值给父类类型的指针。
实现多态,有两种方式,覆盖(重写)和重载。两者的区别在于:覆盖在运行时决定重载是在编译时决定并且覆盖和重载的机制不同。例如在 Java 中,重载方法的签名必须不同于原先方法的,但对于覆盖签名必须相同。


抽象

抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。


优点:

  • 复用性好;
  • 低耦合,修改逻辑时只需修改接口内容;
  • 代码逻辑好理解,更好维护。

1.4 异常

https://blog.csdn.net/justloveyou_/article/details/52551712

所有异常类型的根类为 Throwable,具体包括两大类:Error 与 Exception。其中,Error是指程序无法处理的错误,表示运行应用程序中较严重问题;Exception是指程序本身可以处理的错误,具体可分为运行时异常(派生于 RuntimeException 的异常) 和 其他异常。

划分这两种异常的规则是:由程序错误(一般是逻辑错误,如错误的类型转换、数组越界等,应该避免)导致的异常属于RuntimeException;而程序本身没有问题,但由于诸如I/O这类错误(eg:试图打开一个不存在的文件)导致的异常就属于其他异常。

此外,从异常是否必须需要被处理的角度来看,异常又可分为不受检查异常和受检查异常两种情况:

  • 不受检查异常:派生于 Error 或 RuntimeException 的所有异常;
  • 受检查异常:除去不受检查异常的所有异常。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gl3kfIws-1602775315906)(C:\Users\vocyd\AppData\Roaming\Typora\typora-user-images\image-20200911102422806.png)]


异常处理:

在 Java 应用程序中,异常处理机制为:抛出异常捕捉异常

  • 抛出异常: 当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处理异常的代码并执行。

  • 捕获异常: 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。**潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。**当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止。


throw和throws:

  • throw是语句主动抛出一个异常实例。用在方法体内,表示抛出异常,由方法体内的语句处理。
  • throws是方法可能抛出异常的声明,用在声明方法时,表示该方法可能要抛出异常。
  • throw出现在函数体,throws出现在方法函数头。
  • 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

finally:

finally 关键字用来创建在 try 代码块后面执行的代码块,无论是否发生异常,finally 代码块中的代码总会被执行。在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。

catch和finally可以分别省略,不能同时省略。

执行的保证:

  • try被执行了;
  • try中没有System.exit(0)这样的语句。

执行细节:

  • finally语句在return语句执行之后,返回之前执行。
  • finally块中的return语句会覆盖try块中的return返回。
  • 对于try、catch中的return,在catchreturn之前会将当前引用赋值给局部变量表的一个临时slot中,后续即便finally中的修改其引用但返回的仍是slot中暂存的值。

1.5 反射

https://www.jianshu.com/p/9be58ee20dee

RTTI

RTTI 即 Runtime Type Information,顾名思义,也就是在运行时,识别对象和类的信息。RTTI 有两种,一种是“传统的” RTTI,它假定我们在编译时就已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。

理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似类型转换这样的操作。

每个类都会产生一个对应的Class对象,也就是保存在.class文件。所有类都是在对其第一次使用时,动态加载到JVM的,当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。

类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。

想在运行时使用类型信息,必须获取对象(比如类Base对象)的Class对象的引用,使用功能Class.forName(“Base”)可以实现该目的,或者使用base.class。注意,有一点很有趣,使用功能.class来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象。为了使用类而做的准备工作一般有以下3个步骤:

  • 加载:由类加载器完成,找到对应的字节码,创建一个Class对象;
  • 链接:验证类中的字节码,为静态域分配空间;
  • 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块、

如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,用以表示未知类里对应的成员。这样的话就可以使用Contructor创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,以返回表示字段、方法、以及构造器对象的数组,这样,对象信息可以在运行时被完全确定下来,而在编译时不需要知道关于类的任何事情。

反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

  • RTTI,编译器在编译时打开和检查.class文件。
  • 反射,运行时打开和检查.class文件。

反射:

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

反射不符:经过层层调用后在最终返回结果的地方对应用的权限进行了校验,对于没有权限的应用返回值是没有意义的缺省值,否则返回实际值起到保护用户的隐私目的。


与Java反射相关的类如下:

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造方法

在Class类中,带有Declared修饰的方法可以反射到私有的方法,没有Declared修饰的只能用来反射公有的方法。


1.6 更多问题

1.6.1 Math.round(-1.5)

round()是Math类中的一个四舍五入的方法,Math类中对这个方法有重载,可以传入一个double类型的参数返回的是一个long类型的数值,也可以传入一个float类型的参数返回的是一个int类型的数值。

round()方法在实现四舍五入的时候实际上是不论正负的,它会先将这个数加上0.5然后在向下取整。

所以Math.round(-1.5)的值为-1。


1.6.2 new String和直接初始化值

String str=“i”:

这句话的意思是把“i”这个值在内存中的地址赋给str,如果再有String str3=“i”,那么这句话的操作也是把“i”这个值在内存中的地址赋给str3,这两个引用的是同一个地址值,他们两个共享同一个内存。

String str2 = new String(“i”):

则是将new String(“i”)的对象地址赋给str2,需要注意的是这句话是新创建了一个对象。如果再有String str4= new String(“i”);那么相当于又创建了一个新的对象,然后将对象的地址值赋给str4,虽然str2的值和str4的值是相同的,但是他们依然不是同一个对象了。

需要注意的是:String str=“i”, 因为String 是final类型的,所以“i”应该是在常量池。而new String(“i”)则是新建对象放到堆内存中。


1.6.3 抽象类
  • 抽象类不必须有抽象方法;如果一个类中有抽象方法,这个类必须被声明为抽象类;
  • 抽象类不能被final修饰,因为抽象类创建出来就是被用于继承的,final修饰代表不可修改、不可继承的。

抽象类和接口:都不能实例化对象,都可以包含抽象方法,而且抽象方法必须被继承的类全部实现。

区别:

  1. 抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
  2. 抽象类要被子类继承,接口要被类实现。
  3. 接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。
  4. 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
  5. 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
  6. 抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果。
  7. 抽象类里可以没有抽象方法。
  8. 如果一个类里有抽象方法,那么这个类只能是抽象类。
  9. 抽象方法要被实现,所以不能是静态的,也不能是私有的。
  10. 接口可继承接口,并可多继承接口,但类只能单根继承。

1.6.4 IO流

从流的角度去划分:

  • 按照流的流向分,可以分为输入流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的角色划分为节点流(可以从或向一个特定的地方(节点)读写数据。如FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写,如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数)。

所有流的基类

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

close( )和flush( )的区别:

  • close( ) 关闭流对象,但是先刷新一次缓冲区。关闭之后,流对象不可以继续再使用了。
  • flush( ) 仅仅刷新缓冲区,刷新之后,流对象还可以继续使用。

序列化、反序列化:

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。

  1. 使用Serializable序列化/反序列化:将实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,序列化可以弥补不同操作系统之间的差异。其中,需要注意以下几点:
    • 需要序列化的对象必须实现Serializable接口;
    • 只有非static字段和非transient字段进行序列化,与字段的可见性无关;
    • 序列化/反序列化的实质上操纵的是一个对象图;
  2. 使用XML、JSON 等进行序列化。

1.6.5 final、finally、finalize

final:

final是Java中的关键字,终态修饰符。用于声明属性,方法和类,分别表示属性不可该变,方法不可覆盖(重写),类不可继承。

  • 定义变量:被声明为final的变量必须在声明时初始化,而在以后的引用中只能读取,不可修改。
  • 定义方法:当final用来定义一个方法时,它表示这个方法不可以被子类重写,但是并不影响它被子类继承。
  • 定义类:final类不允许被继承,编译器在处理时把它的所方法都当作final的,因此final类比普通类拥更高的效率。而由关键字abstract定义的抽象类含必须由继承自它的子类重载实现的抽象方法,因此无法同时用final和abstract来修饰同一个类。同样的道理,final也不能用来修饰接口。

finally:

见1.5 异常,finally会在try、catch的控制转移语句执行前执行。


finalize:

finalize,它是一个方法,属于java.lang.Object类,是GC运行机制的一部分,在GC清理它所从属的对象时被调用,如果执行它的过程中抛出了无法捕获的异常(uncaughtexception),GC将终止对改对象的清理,并且该异常会被忽略;直到下一次GC开始清理这个对象时,它的finalize()会被再次调用。


1.6.6 BIO、NIO、AIO

https://blog.csdn.net/m0_38109046/article/details/89449305

  • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
  • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

1.6.7 克隆

为什么需要克隆

克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。而clone是一个native方法,非Java语言实现,就是快啊,在底层实现的。

我们常见的Object a=new Object(); Object b; b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。而通过clone方法赋值的对象跟原来的对象时同时独立存在的。


浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。


深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值