面向对象基础
面向对象与面向过程的区别
面向过程把解决问题的过程拆解成一个个方法,通过一个个方法的执行来解决问题。
面向对象会先抽象出对象,再用对象执行方法的方式来解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展。
创建一个对象用什么运算符?对象实体与对象引用有什么不同
new运算符,对象实例存在于堆内存中,对象引用指向对象实例,对象引用存放在栈内存中。
一个对象引用可以指向0或1个对象(一个绳系一个气球
一个对象可以有n个引用指向它(一个气球可以有多个绳系着
对象相等和引用相等的区别?
对象的相等一般是比较内存中存放的内容是否相等
引用的相等一般比较的是他们指向的内存地址是否相等
==比较的是引用是否相等(内存地址
equals()比较的是内容是否相等
如果一个类没有声明构造方法,程序能正常执行吗?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作
如果一个类没有声明构造方法也可以正常执行,因为java会提供默认的无参构造方法。如果我们自己添加了类的构造方法(无论是否有参),java就不会提供默认构造方法。
如果我们重载了有参构造方法,一定要把无参构造方法写出来,不然会编译出错。
构造方法有哪些特点?是否可以被override?
名字与类名相同
没有返回值,但是不能用void声明构造方法
生成类的对象时,自动执行,不用调用
不能被重写(override),但是可以被重载overload)
面向对象三大特征?
封装
就是把一个对象的状态信息(属性)隐藏在对象内部,不允许外部对象直接访问内部信息,但是可以提供一些能被外界访问的方法来操作属性。
继承
是使用已存在的类的定义作为基础建立新的类,新的类可以增加新的属性或者新的功能,也可以用父类的功能,但是不能选择性的继承父类。
通过继承,可以快速创建新的类,提高代码的重用,程序的可维护性,节省创建新类的时间,提高开发效率。
关于继承:
子类拥有父类的所有属性和方法(包括私有属性、方法),但是父类的私有属性、方法子类不能访问,只是拥有。
子类可以拥有自己的属性、方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态
就是指一个对象具有多种状态,具体表现为父类的引用指向子类的对象。
特点:
对象类型和引用类型之间具有继承(类)关系/实现(接口)关系
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
多态不能调用只在子类存在但在父类不存在的方法
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果没有覆盖父类方法,执行的是父类的方法
接口和抽象类有什么共同点和区别?
共同点:
都不能被实例化
都可以包含抽象方法
都可以有默认实现的方法
区别:
接口主要是对类的行为进行约束,你实现了某个接口就具有了对应的行为
抽象类主要用于代码复用,强调的是所属关系
一个类只能继承一个类,但可以实现多个接口
接口中的成员变量必须是public static final类型的,不能被修改且必须有初始值
抽象类的成员变量默认是default,可以在子类中被重新定义,也可以重新赋值
深拷贝和浅拷贝了解吗?什么是引用拷贝?
浅拷贝:会在堆上创建一个新的对象,不过如果原来对象的内部属性是引用类型的话,浅拷贝会直接复制内部对象的地址,就是说“浅拷贝对象和原对象共用一个内部对象”
深拷贝:会完全复制整个对象,包括原对象所包含的内部对象
引用拷贝:就是两个不同的引用,指向同一个对象
Object
Object类的常见方法有哪些?
Object类是一个特殊的类,是所有类的父类。
主要提供了以下11个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允
许子类重写。
*/
public final native Class<?> getClass() /**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode() /**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的 值是否相等。
*/
public boolean equals(Object obj) /**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException /**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString() /**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的 概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify() /**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等 待的所有线程,而不是一个线程。
*/
public final native void notifyAll() /**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方 法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException /**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时 的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException /**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException /**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
==和equals()的区别?
对于基本数据类型,==比较的是值
对于引用数据类型,==比较的是对象的地址值
(因为java只有值传递,所以不管是基本数据类型还是引用数据类型,==本质比较的都是值,只是引用数据类型变量存的值是对象的地址值)
equals()不能判断基本数据类型的变量,只能判断两个对象是否相等。
equals()方法存在于Object类中,而Object是所有类的直接或者间接父类,所以所有的类都有equals()方法。
存在两种情况:
类没重写equals()方法:等同于==比较两个对象地址是否相同
类重写了equals()方法:比较两个对象的属性是否相同
hashCode()有什么用?
hashCode()用来获取哈希码(int整数),也称为散列码。
哈希码作用是确定该对象在哈希表中的索引位置。
hashCode()定义在Object类中,java中所有类都拥有hashCode()方法,这个方法是用C语言或者C++实现的。
散列表存储的是键值对(key-value),特点是:能根据键快速检索出对应的值。这就利用到了散列码,可以快速的找到所需要的对象。
为什么要有hashCode()?
因为当我们把对象加入HashSet时,会用到hashCode。首先要计算出对象的hashCode值,来判断对象加入的位置,同时也会拿这个值与已经加入的对象的hashCode值作比较,如果没有相符的hashCode值,就假设对象没有重复出现。但是如果发现有相同的hashCode值的对象,这时就会调用equals()方法来检查这两个hashCode值相等的对象是否真的相同。如果两者相同,hashSet就不会让这个对象加入,如果不同,就会重新散列到其他位置,这样就大大减少了equals的次数,相应的提高了执行速度。
如果hashSet在对比的时候,同样的hashCode有多个对象,它会继续用equals()来判断是否真的相同。所以hashCode()缩小了查找成本。
其实equals()和hashCode()都是用于比较两个对象是否相等。
那为什么JDK还要提供这两个方法呢?
这是因为在一些容器中(比如hashMap、hashSet)中,有了hashCode()之后,判断元素是否在容器中的效率会更高。
那为什么不只提供hashCode()方法呢?
因为两个对象的hashCode值相同不代表两个对象就相等。
那为什么两个对象有相同的hashCode值,它们也不一定相等呢?
因为hashCode()所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。
越糟糕的哈希算法越容易碰撞(所谓的哈希碰撞就是指不同的对象得到相同的hashCode值)。
总结:
如果两个对象的hashCode值相等,那这两个对象不一定相等(哈希碰撞)
如果两个对象的hashCode值相等,并且equals()方法也返回true,才认为这两个对象相等
如果两个对象的hashCode值不相等,可以直接认为这两个对象不相等
为什么重写equals()方法必须重写hashCode()方法?
因为两个相等的对象的hashCode值必须是相等的,也就是说如果equals方法判断这个两个对象相等,那么这两个对象的hashCode值也要相等。
如果只重写了equals()方法,可能会导致equals()方法判断是相等的两个对象,它们的hashCode值不同。
思考:重写equals()没重写hashCode(),使用hashMap会出现什么问题?
String
String、StringBuffer、SyringBuilder的区别?
可变性:
String是不可变的。
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,在AbstractSyringBuilder中也是使用字符数组保存字符串,不过没有使用final和private修饰,AbstractStringBuilder还提供了很多修改字符串的方法,比如append、insert、indexOf、expandCapacity方法。
线程安全性:
String中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer对方法加了同步锁,或者对调用的方法加了同步锁,所以线程安全。
StringBuilder没有对方法加同步锁,所以线程不安全。
性能:
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。
StringBuffer每次会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下,使用StringBuilder相比使用StringBuffer仅能获得10%-15%左右的性能提升,却要冒多线程不安全的风险。
总结:
操作少量数据:String
单线程操作字符缓冲区下操作大量数据:StribgBuilder
多线程操作字符缓冲区下操作大量数据:StringBuffer
String为什么是不可变的?
String类中使用final关键字修饰字符数组来保存字符串,并且是私有的,也没有提供修改这个字符串的方法,而且String类被final修饰也不可继承。
Java9为什么要将String的底层实现由char[]改成了byte[]?
节省内存空间。(Latin-1编码方案下,byte占一个字节(8位),char占两个字节(16位)
字符串拼接用“+”还是String Builder?
Java语言本身并不支持运算符重载,“+”“+=”是专门为String类重载过的运算符,也是Java中仅有的两个重载过的运算符。
可以看出,字符串对象通过+的字符串拼接方式,实际上是通过StringBuilder调用append()方法实现的,拼接完成之后调用toString()得到一个String对象。
不过,在循环内用+进行字符串拼接的话,会有个缺陷:编译器不会创建单个StringBuilder然后复用,就会导致创建过多的StringBuilder对象。
如果直接使用StringBuilder对象进行字符串拼接就不会存在这个问题。
String的equals()和Object的equals()有什么区别?
String中的equals方法是被重写过的,比较的是String字符串的值是否相等。
Object的equals方法是比较对象的内存地址。
字符串常量池的作用了解吗?
字符串常量池是JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s = new String("abc") 创建了几个字符串对象?
一个或者两个。
如果字符串常量池中不存在字符串对象"abc"的引用,那么会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
如果字符串常量池中已经存在字符串对象"abc"的引用,就只会创建一个字符串对象"abc"。
String的intern()方法有什么用?
String.intern()是一个native(本地)方法,作用是将指定的字符串对象的引用保存在字符串常量池中。
有两种情况:
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用
如果字符串常量池中没有保存对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回
String类型的变量和常量做“+”运算时发生了什么?
两种情况:
有final修饰:
无final修饰:
“+”实际上是通过StringBuilder调用append()方法实现拼接,然后再调用toString()得到新的String对象。