Java基础总结

一、语法

8种基本数据类型

byte、short、int、long、float、double、char、boolean

String 不是基本类型而是对象;
int 的取值范围是20亿左右;
long 初始化时要在后面加上L;

引用数据类型:Class、Interface、Array

自动拆箱和装箱

基本类型对应的包装类:Byte、Short、Integer、Long、Float、Double、Character、Boolean;
装箱过程:调用包装器的 valueOf 方法实现;
拆箱过程:调用包装器的 xxxValue 方法实现;

Integer比较大小应该使用 equals。因为对于-128至127范围内的Integer对象,会使用.cache进行缓存,值相同的integer对象都是指向的同一块内存空间,所以这个区间内的Integer值可以直接使用==进行判断,但是超过这个范围就不等了,而使用equals比较的是对象的值,只要值相同的Integer对象都相等。

Integer i = new Integer(xxx) 和 Integer i = xxx的区别?
(1)第一种方式不会触发自动装箱的过程;第二种方式会触发。
(2)在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般情况下要优于第一种情况。(注意这不是绝对)

Integer a=1;
Integer b=2;
Integer c=3;
Long d=3L;
System.out.println(c == d); // 提示出错,不可比较。说明此时没有自动拆箱
// 遇到运算符才会自动拆箱
System.out.println(d == (a+b)); // true
System.out.println(d.equals(a+b)); // false

当 == 运算符的两个操作数都是 包装器类型,则是比较指向的是否是同一个对象;
而如果其中有一个操作数是表达式(即包含算数运算),则比较的是数值(即会触发自动拆箱的过程)。
另外,对于包装器类型,equals方法并不会进行类型转换。

拓展(引申到JVM)

String 声明的字面量数据都放在字符串常量池中

JDK 1.6  // 字符串常量池存放在方法区中(即永久代中)
String str = new String("aaa"); // 在堆中创建一个aaa对象,然后在栈中创建str的引用指向堆中的"aaa"
str.intern(); // 在常量池中创建aaa对象
String str1 = "aaa"; // 在栈中创建str1指向常量池中的aaa
// 所以 str 和 str1 的地址是不一样的
System.out.println(str == str1); // false
JDK 1.8  // jdk7以后字符串常量池存放在堆空间中
String str = new String("aaa"); // 堆空间对象引用
String str1 = "aaa"; // 常量池引用(也是在堆空间中)
System.out.println(str == str1); // false

String str = new String("aaa"); // 在堆中创建一个aaa对象,然后在栈中创建str的引用指向堆中的"aaa"
str.intern(); // 此时常量池里面没有aaa,因为常量池也在堆空间中,也就不会生成新的aaa,直接在常量池中存放堆空间aaa的地址
String str1 = "aaa"; // 这时栈中的str1也是指向堆空间中aaa的地址
// 所以二者地址相等
System.out.println(str == str1); // true

String str = new String("aaa"); // 在堆空间中创建一个aaa
String str1 = "aaa"; // 在常量池里面生成一个aaa
str.intern(); // 常量池里已经有aaa了,所以不会共用一个地址
System.out.println(str == str1); // false

String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用,否则,将新的字符串放入常量池,并返回新字符串的引用

变量

关键字

主要有:final、synchronized、static、transient、volatile、instanceof、super、this

final:

  • 用于修饰数据:包括成员变量和局部变量,只能被赋值一次且它的值无法被改变。对于实例变量来讲:
    修饰类变量,必须在声明时初始化;
    修饰实例变量,必须在声明时或者构造方法种对它赋值。
  • 用来修饰方法参数:表示在变量的生存期中它的值不能被改变。
  • 修饰方法:表示该方法无法被重写。
  • 修饰类:表示该类无法被继承。

String 初始化后不可变,其底层实现:private final char value[]

String s1="abc"; // 创建一个字符串对象"abc"在常量池中。
String s2= new String("abc"); // 创建两个对象,一个new对象和一个字符串对象在堆内存中。

static:

  • 修饰变量:因为类加载进方法区,所以多个对象是共享的;(引申:JVM类加载过程)
  • 修饰方法:工具类的方法,不需要建立对象,直接使用“类名.方法名”的方式调用;
  • 修饰静态代码块:只会在类被初始化的时候执行一次,可以用于初始化等操作;
  • 静态内部类
    (1)在创建静态内部类的实例时,不需要创建外部类的实例。
    public class Outer {
      static class Inner {
      }
    }
    class OtherClass {
      Outer.Inner oi = new Outer.Inner();
    }
    (2)静态内部类中可以定义静态成员和实例成员。
    外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成;
    如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
    public class Outer {
      static class Inner {
          int a = 0;    // 实例变量a
          static int b = 0;    // 静态变量 b
      }
    }
    class OtherClass {
      Outer.Inner oi = new Outer.Inner();
      int a2 = oi.a;    // 访问实例成员
      int b2 = Outer.Inner.b;    // 访问静态成员
    }
    (3)静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
    public class Outer {
      int a = 0;    // 实例变量(对象变量)
      static int b = 0;    // 静态变量(类变量)
      static class Inner {
          Outer o = new Outer;
          int a2 = o.a;    // 访问实例变量
          int b2 = b;    // 访问静态变量
      }
    }

一般方法可以访问静态方法,但是静态的必须访问静态的。
静态属性和静态方法可以被继承,但是不能被重写。

super和this用法和区别:

super 可以理解为是指向自己父类对象的一个指针,而这个父类指的是离自己最近的一个父类。
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

Comparable和Comparator区别

  • Comparable:内部比较器,比较属性,静态绑定。

    public interface Comparable<T> {
      public int compareTo(T o);
    }

    Comparable 对实现它的每个类的对象进行整体排序,这个接口的方法需要类本身去具体实现。一般来说,如果在创建一个类时,如果这个类具有排序的属性,那么可以去实现这个comparable接口,并override其ComparableTo方法。

  • Comparator:外部比较器,比较器,动态绑定。

    public interface Comparator<T> {
      int compare(T o1, T o2);
      boolean equals(Object obj);
    }

    如果一个类已经没法修改,那可以采用外部比较器。

参数的传递机制

  • 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底时值传递还是引用传递?

是值传递。Java 中只有值传递参数。

  • 如果参数类型是基本数据类型,那么传过来的就是这个参数的副本,也就是这个原始参数的值,如果在函数中改变了副本的值,,不会改变原始的值。
  • 如果参数类型是引用类型(对象的引用),那么传过来的就是这个引用参数的副本,这个副本存放的是参数的地址。如果在函数中没有改变这个副本的地址,而是改变了地址中的值,那么在函数内的改变会影响到传入的参数(比如:传入对象的引用,如果函数内对对象的属性进行了修改,那么传入的对象属性发生改变)。如果在函数中改变了副本的地址,如new一个,那么副本就指向了一个新的地址,此时传入的参数还是指向原来的地址,所以不会改变参数的值。

总之,不管传递什么类型的参数,都是传递的副本,原始类型就传递值的副本,引用类型就是传递地址的副本。

  • 如果在方法内修改了地址指向的内容,那么就会影响传入地址的内容(浅拷贝)。
  • 如果在方法内new了一个新的指向,副本指向新的内容,那么副本修改不会改变原地址的内容(深拷贝)。

String和StringBuilder、StringBuffer的区别

  • String:不可变,线程安全;
  • StringBuffer:可变,线程安全(因为内部使用synchronized);
  • StringBuilder:可变,线程不安全;

为什么说 String 类是 final 修饰的?
1.为了实现字符串池。因为只有当字符串是不可变的,字符串池才有可能实现。
2.为了线程安全。
3.为了实现 String 可以创建 hashcode 不可变性。因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。

Object类有哪些方法

  • clone 方法:protected 方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法。主要是java里除了8种基本类型传参数是值传递,其他类对象传参数都是引用传递,我们有时候不希望在方法里将参数改变,这时就需要在类中重写clone 方法。
  • getClass 方法:final 方法,获得运行时类型。
  • toString 方法:
  • finalize 方法:该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
    为什么不能显示直接调用 finalize 方法?
    finalize 方法在垃圾回收时一定会执行,而如果在此之前显示执行的话,也就是说 finalize 会被执行两次以上,而在第一次资源已经被释放,那么在第二次释放资源时系统一定会报错,因此一般 finalize 方法的访问权限和父类保持一致,为 protected。
  • equals 方法:Object 中就是 ==,比较地址相等。
  • hashCode 方法(hashmap源码实现):该方法用于哈希查找,可以减少在查找中使用 equals 的次数,重写了 equals 方法一般都要重写 hashCode 方法。这个方法在一些具有哈希功能的 Collection 中用到。
  • wait 方法(为什么操作线程的方***放在 Object 对象中?):wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
  • notify 方法:该方法唤醒在该对象上等待的某个线程。
  • notifyAll 方法:该方法唤醒在该对象上等待的所有线程。

Java的反射

  • 概念:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。(动态代理设计模式也采用了反射机制)

优点:运行期类型的判断,动态加载类,提高代码灵活度。
缺点:性能瓶颈,反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多。安全问题:可以动态操作改变类的属性同时也增加了类的安全隐患。

  • Class.forName 和 classloader 的区别(结合 JVM 类加载)
    Class.forName 除了将类的 .class 文件加载到 JVM 中之外,还会对类进行解释,执行类中的 static 块。
    而 classloader 只干一件事,就是将 .class 文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。forName("")得到的 class 是已经初始化完成的。
    最重要的区别是:forName 会初始化 Class,而 loadClass 不会。因此如果要求加载时类的静态变量被初始化或静态块里的代码被执行就只能用 forName,而用 loadClass 只有等创建类实例时才会进行这些初始化。

异常

Exception 分为两种:

  • 运行时异常(不受检异常):RuntimeException 类及其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,

  • 编译异常(受检异常):Exception 中除 RuntimeException 及其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理(try catch、throw),否则编译不通过。

IO、NIO、AIO的区别

同步与异步(被调用者):

  • 同步:同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步:异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。

同步和异步的区别在于异步的被调用者会通过回调等机制来获得调用者的返回结果。

阻塞与非阻塞(调用者):

  • 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回。

如何理解同步阻塞、同步非阻塞和异步非阻塞:
同步阻塞:进行一个调用时,就一直等着返回;如果调用不返回就一直等待。
同步非阻塞:进行一个调用时,会切出去干别的事情,只需要每隔一段时间来查看一下调用有没有返回。
异步非阻塞:进行一个调用时,会切出去干别的事情,只需要等待调用返回的信号,再回来进行处理。

BIO(Blocking I/O):

同步阻塞IO模型,数据的读取写入必须阻塞在一个线程内等待其完成。
图片说明
采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。
如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()、socket.read()、socket.write()
涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型。

NIO(New I/O):

NIO 是一种同步非阻塞的 I/O 模型,在Java 1.4中引入了NIO框架,对应 Java.nio 包,提供了 Channel,Selector,Buffer等抽象(Netty 对自带的 NIO 的API 进行了封装,目前主流的网络通信框架)。NIO 中的N可以理解为 Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的 I/O 操作方法。(操作系统的IO多路复用)
面试时:从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性(通道、缓存、选择器)为 NIO 带来的一些改进来分析。
图片说明

  • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Asynchronous IO(异步IO):Java NIO 可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器):JavaNIO 引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
  • IO(BIO)与NIO的区别:
    (1)面向流与面向缓冲:IO 是面向流的,NIO 是面向缓冲的。
    Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
    Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所需处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
    (2)阻塞与非阻塞IO:
    Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
    Java NIO 的非阻塞模式,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据,写数据也是一样的。
    (3)选择器(Selectors):
    Java NIO 的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来选择通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这个选择机制,使得一个单独的线程很容易来管理多个通道。

AIO(Asynchronous I/O):

AIO 时在 Java 7 中引入了 NIO 的改进版,异步非阻塞的IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

  • 如何理解 NIO同步 和 AIO异步?
    虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,业务流程是在 IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。(深入理解需要和操作系统IO一起学习)

二、面向对象

三大特征

  • 封装:
    在面向对象思想中,封装指数据(类成员属性)和对数据的操作(类的方法)捆绑到一起,形成对外界的隐藏,同时对外提供可以操作的接口(供外部访问的类成员)。
    封装的意义在于保护或者防止代码(数据)被我们无意中破坏,保护成员属性,不让类以外的程序直接访问和修改,隐藏方法细节。

  • 继承:
    继承是从已有的类中派生出新的类,新的类能吸收已有类的属性和行为,并能扩展新的能力。为什么要继承:反映现实的真实关系,减少代码冗余,对父类的属性和方法进行扩展和重写。继承中,子类不可以选择性的继承父类的东西,而是全部继承父类的属性和方法。其中父类又叫超类或基类,子类又叫派生类。父类是子类的一般化,子类是父类的具体化。Java中不支持多继承,一个类最多只能有一个父类。而在Java中多继承是通过接口实现的。

  • 多态:
    父类引用指向不同子类对象。
    Java 实现多态有三个必要条件:继承、重写、向上转型。
    继承:在多态中必须存在有继承关系的子类和父类。
    重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
    向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备能调用父类的方法和子类的方法。
    实现方式:接口多态性,继承多态性,抽象类实现的多态性。
    实现原理:动态绑定。

继承和组合的区别和选择

继承

继承是 Is-a 的关系,比如说 Student 继承 Person,则说明 Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

  • 优点:
    (1)容易进行新的实现,因为其大多数可继承而来;
    (2)易于修改或扩展那些被复用的实现;
  • 缺点:
    (1)父类的内部细节对子类是可见的;
    (2)子类从父类继承的方法在编译时就确定下来了, 所以无法在运行期间改变从父类继承的方法的行为;
    (3)如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

组合

组合是 has-a 的关系,组合就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

  • 优点:
    (1)当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的;
    (2)当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码;
    (3)当前对象可以在运行时动态的绑定所包含的对象。可以通过 set 方法给所包含对象赋值。
  • 缺点:
    (1)容易产生过多的对象;
    (2)为了能组合多个对象,必须仔细对接口进行定义。

组合和继承如何选择?

组合比继承更具灵活性和稳定性,所以在设计的时候优先使用组合。只有当下列条件满足时才考虑使用继承:
(1)子类是一种特殊的类型(String 类、Object 类、包装类),而不只是父类的一个角***r>(2)子类的实例不需要变成另一个类的对象;
(3)子类是父类的扩展。

为什么优先选择组合?

  • 扩展性:继承是在编译时刻静态定义的,即是静态复用,在编译后子类和父类的关系就已经确定了。而组合这是运用于复杂的设计,它们之间的关系是在运行时候才确定的,即在对对象没用创建运行前,整体类是不会知道自己将持有特定接口下的那个实现类。在扩展方面组合比继承更具有广泛性。

  • 单一性:继承中父类定义了子类的部分实现,而子类中又会重写这些实现,修改父类的实现,在设计模式中认为这是一种破坏了父类的封装性的表现。这个结构导致的结果是父类实现的任何变化,必然导致子类的改变。然而组合则不会出现这种现象。对象的组合就是有助于保持每个类被封装,并被集中在单个任务上(符合类设计的单一原则)。这样类的层次结构不会扩大,一般不会出现不可控的庞然大类。而类的继承就可能出来这些问题,所以一般编码规范要求类的层次结构不要超过3层。组合是大型系统软件实现即插即用时的首选方式。

  • 按需选择:“优先使用对象组合,而不是继承”是面向对象设计的第二原则,但并不是说什么都设计都用组合,只是优先考虑组合,更不是说继承是不好的设计,应该根据他们之间各自的优势进行选择。

重载和重写的区别(多态的体现):

  • 重载(overloading):同一类中同名函数,具有不同参数个数或类型(返回值不参与),是一个类中多态性的体现。是由静态类型确定,在类加载的时候就确定,属于静态分派。

  • 重写(overriding):子类中含有与父类相同名字、返回类型和参数表,则重写,是在继承中多态性的体现,属于动态分派。

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

构造器Constructor是否可被重写

构造器不能被继承,所以不能重写override,但是可以被重载overload(不同参数即可)。
如果父类有无参构造器,则在子类的构造器中用super调用父类构造器不是必须的;
如果没用使用super关键字,系统会自动调用父类的无参构造器(隐藏了super())。
即:
(若父类有参数的构造器,自子类必须用super调用并配上参数;若父类有无参数的构造器,子类不用super,系统自动调用无参构造器)。

==和equals的区别

  • ==:如果是基本数据类型,则直接对值进行比较;如果是引用数据类型,则是对他们的地址进行比较;(遇到运算符才会自动拆箱)
  • equals:euqals方法继承自 Object 类,在具体实现时可以覆盖父类中的实现。看一下 Object 中 equals 的源码发现,它的实现也是对对象的地址进行比较,本质就是 == ,而JDK类中有一些类覆盖了Object类的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:Java.io.file,file.util.Date,Java.lang.string,包装类。

在实际中:
(1)类未重写equals 方法,则使用 equals 方法比较两个对象时,相当于==比较,即两个对象的地址是否相等。地址相等,返回true,地址不相等,返回false。
(2)类重写 equals 方法,比较两个对象时,则走重写之后的判断方式。通常,我们会将 equals 重写成:当两个对象内容相同时,则 equals 返回true,内容不同时,返回false。

判断两个对象相等

  • 明确equals的作用及与==的区别。

  • 明确hashCode的作用及与equals的关系:
    对于利用hash进行存储对象的集合,比如 hashmap、hashset 等,还要重写 hashcode 值,比如 set 判断两个元素相等,会判断 hashcode 和 equals 都相等,则认为相等,不会添加新元素。 (hashCode是用来在散列存储结构中确定对象的存储地址的)

  • equals 方法与 hashcode 关系:
    (1)如果两个对象 equals 相等,这两个对象的 hashcode 一定相等, hashcode 不相等的两个对象,equals一定不相等。
    (2)如果两个对象 hashcode 相等,这两个对象未必相等,只能说这两个对象存储在一个地址上(称为hash碰撞)。
    (3)如果对象的 equals 被重写,那么对象的 hashcode 方法也要被重写。

  • hashmap 底层是如何利用 hashcode 的?

为什么重写equals时必须重写hashCode方法

如果重写了 equals() 而未重写 hashcode() 方法,可能就会出现两个没有关系的对象 equals 相同(因为equal都是根据对象的特征进行重写的),但 hashcode 不相同的情况。由于默认的 hashcode 方法是根据对象的内存地址经哈希算法得来的,故两者的 hashcode 值不一定相等。

深拷贝和浅拷贝的区别

  • 引用拷贝:创建一个指向对象的引用变量的拷贝。

    public static void main(String[] args) {
      Teacher teacher = new Teacher("riemann", 28);
      Teacher otherTeacher = teacher;
      // 地址相同,为同一个对象。teacher和otherTeacher只是引用而已,指向了相同的对象
      System.out.println(teacher); // com.test.Teacher@28a418fc
      System.out.println(otherTeacher); // com.test.Teacher@28a418fc
    }
    class Teacher {
      private String name;
      private int age;
    
      public Teacher(String name, int age) {
          this.name = name;
          this.age = age;
      }
    }

    引用拷贝

  • 对象拷贝:创建对象本身的一个副本。

    public static void main(String[] args) {
      Teacher teacher = new Teacher("riemann", 28);
      Teacher otherTeacher = (Teacher) teacher.clone();
      // 地址不同,创建了一个新的对象,而不是把原对象的地址赋给一个新的引用变量
      System.out.println(teacher); // com.test.Teacher@28a418fc
      System.out.println(otherTeacher); // com.test.Teacher@5305068a
    }
    class Teacher {
      private String name;
      private int age;
    
      public Teacher(String name, int age) {
          this.name = name;
          this.age = age;
      }
      public Object clone() throws CloneNotSupportedException {
          Object object = super.clone();
          return object;
      }
    }

    对象拷贝

浅拷贝和深拷贝都属于对象拷贝。

  • 浅拷贝:如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用地址的传递,而没有真实的创建一个新的对象。即浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。

    public static void main(String[] args) {
      Teacher teacher = new Teacher("riemann", 28);
      Student student1 = new Student();
      student1.setTeacher(teacher)
      Student student2 = (Student) student1.clone();
    }
    class Teacher implements Cloneable {
      private String name;
      private int age;
    
      public Teacher(String name, int age) {
          this.name = name;
          this.age = age;
      }
    }
    class Student implements Cloneable {
      private String name;
      private int age;
      private Teacher teacher;
    
      public void setTeacher(Teacher teacher) {
          this.teacher = teacher;
      }
      public Object clone() throws CloneNotSupportedException {
          Object object = super.clone();
          return object;
      }
    }

    两个引用 student1 和 student2 指向不同的两个对象,但是两个引用 student1 和 student2 中的两个 teacher 引用指向的是同一个对象。
    图片说明

  • 深拷贝:在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量。当对象和它所引用的对象一起拷贝时,即发生了深拷贝,深拷贝相比于浅拷贝速度较慢并且花销较大。即深拷贝把要复制的对象所引用的对象都复制了一遍。

    public static void main(String[] args) {
      Teacher teacher = new Teacher("riemann", 28);
      Student student1 = new Student();
      student1.setTeacher(teacher)
      Student student2 = (Student) student1.clone();
    }
    class Teacher implements Cloneable {
      private String name;
      private int age;
    
      public Teacher(String name, int age) {
          this.name = name;
          this.age = age;
      }
      public Object clone() throws CloneNotSupportedException {
          return super.clone();
      }
    }
    class Student implements Cloneable {
      private String name;
      private int age;
      private Teacher teacher;
    
      public Teacher getTeacher() {
          return teacher;
      }
      public void setTeacher(Teacher teacher) {
          this.teacher = teacher;
      }
      public Object clone() throws CloneNotSupportedException {
          Student student = (Student) super.clone();
          // 将Teacher对象复制一份并重新set进来
          student.setTeacher((Teacher) student.getTeacher().clone());
          return student;
      }
    }

    两个引用 student1 和 student2 指向不同的两个对象,两个引用 student1 和 student2 中的两个 teacher 引用指向的是两个对象,但对 teacher 对象的修改只能影响 student1 对象,所以说是深拷贝。
    图片说明

clone() 方法:是对当前对象进行浅度克隆,只能克隆对象的基本数据类型(包括基本数据类型的包装类)和 String 类型的属性,引用类型依然是在传递引用。
如果要进行一个深拷贝:
(1)继续利用clone()方法,除了对当前对象进行克隆,再对其内的引用类型的变量,再进行一次clone()(即克隆对象持有的引用类型的Class也实现 Cloneable 接口)。
(2)序列化这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写。

小结:实现Cloneable接口的方式,做浅克隆还可以,但是要做深度克隆的话,需要手动地将对象的引用类型进行单独克隆,维护起来比较麻烦,不适合实际当中使用。

序列化和反序列化

序列化:将对象写入到IO流中。
反序列化:从IO流中恢复对象。
序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络运输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行独立存在。

  • 使用场景:所有可在网络上传输的对象都必须是可序列化的,如:RMI(远程方法调用)、RPC(远程过程调用),传入的参数或返回的对象都是可序列化的,否则会出错。所有需要保存到磁盘的Java对象都必须是可序列化的。
    (1)所有需要网络传输的对象都需要实现序列化接口,通过建议所有的 JavaBean 都实现 Serializable 接口。
    (2)对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。如果想让某个变量不被序列化,使用 transient 修饰。
    (3)序列化对象的引用类型成员变量,也必须是可序列化的,否则会报错。反序列化时必须有序列化对象的class文件。
    (4)同一个对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
    (5)建议所有可序列化的类加上 serialVersionUID 版本号,方便项目升级。(如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报 InvalidClassException 异常。)

静态属性和静态方法可以被继承吗

可以被继承但不能被重写,而是隐藏,只有非静态的方法可以被继承并且被重写(override)。

  • 表面原因:
    (1)静态方法和属性是属于类的,调用的时候直接通过 类名.方法名 来完成调用,不需要继承机制及可以调用。如果子类里面定义了静态方法和属性,那么父类的静态方法或属性称之为“隐藏”。如果想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成,至于是否继承一说,子类可以继承静态方法和属性,但是跟非静态方法和属性不太一样,存在“隐藏”的这种情况。
    (2)静态属性、静态方法都可以被继承和隐藏而不能被重写,因此不能实现多态(父类引用指向不同子类对象)。非静态方法可以被继承和重写,可以实现多态。

  • 深入原因:
    静态方法和属性是按“编译时期的类型”进行调用的,而不是按“运行时期的类型”进行调用的,而非static方法,才是按“运行时期的类型”进行调用的。(JVM 中的非虚方法和虚方法理解)

抽象类和接口的区别

  • 抽象类(为了继承而存在):
    (1)抽象方法必须为 public 或者 protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;
    (2)抽象类不能用来创建对象;
    (3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为 abstract 类。
    (4)继承抽象类的关键字为extends

  • 接口(更加抽象):
    (1)变量只能定义为 public static final;
    (2)方法只能为抽象的;
    (3)实现接口的关键字为implements

  • 区别:
    语法层面:
    (1)抽象类可以提供成员方法的实现细节,而接口中只能存在 public abstract 方法;
    (2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
    (3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
    (4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
    设计层面:
    (5)类为“是不是关系”,接口为“有没有关系”

抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

三、集合

ArrayList和LinkedList区别

ArrayList扩容过程

HashMap底层实现

全部评论

相关推荐

vegetable_more_exercise:1-1.5万,没错啊,最少是1人民币,在区间内
点赞 评论 收藏
分享
点赞 1 评论
分享
牛客网
牛客企业服务