面向对象基础
面向对象和面向过程的区别
面向过程
-
优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
-
缺点: 没有面向对象易维护、易复用、易扩展。
面向对象
-
优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
-
缺点: 性能比面向过程低。
两者的主要区别在于解决问题的方式不同:
-
面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
-
面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。另外,面向对象开发的程序一般更易维护、易复用、易扩展。
面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。
创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象;一个对象可以有 n 个引用指向它。
对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
类的构造方法的作用是什么?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作
如果一个类没有声明构造方法,该程序能正确执行吗?
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
构造方法有哪些特点?是否可被 override?
构造方法特点如下:
-
名字与类名相同。
-
没有返回值,但不能用 void 声明构造函数。
-
生成类的对象时自动执行,无需调用。
构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
面向对象三大特征
封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
关于继承如下 3 点请记住:
-
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
-
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
-
子类可以用自己的方式实现父类的方法。
多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
所以对于多态我们可以总结如下:
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
对于面向对象而言,多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
如果想要详细了解这个问题,可以参考这篇文章——面向对象编程三大特性------封装、继承、多态。
Java语言是如何实现多态的?
本质上多态分两种:
- 编译时多态(又称静态多态)
- 运行时多态(又称动态多态)
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行的时候调用的是确定的方法。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。 这也是为什么有时候多态方法又被称为延迟方法的原因。
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型(而不是引用变量的类型)决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
重载(Overload)和重写(Override)的区别是什么?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
-
重写发生在子类与父类之间, 重写方法返回值和形参都不能改变,与方法返回值和访问修饰符无关,即重写的方法不能根据返回类型进行区分。即外壳不变,核心重写!
-
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
重载的方法能否根据返回值类型进行区分?
不能根据返回值类型来区分重载的方法。因为调用时不指定返回值类型信息,编译器不知道你要调用哪个函数。
float max(int a, int b);
int max(int a, int b);
接口和抽象类有什么共同点和区别?
共同点
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别
语法层面上的区别:
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的,不能被修改且必须有初始值;
- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
设计层面上的区别:
-
抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
-
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
-
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
如果想要详细了解这个问题,可以参考这篇文章——深入理解Java的接口和抽象类。
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类。
java 创建对象有哪几种方式?
- new创建新对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
前两者都需要显式地调用构造方法。对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在java中序列化可以通过实现Externalizable或者Serializable来实现。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
-
浅拷贝: 浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
-
深拷贝 : 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable 接口,并重写了 clone() 方法。
clone() 方法的实现很简单,直接调用的是父类 Object 的 clone() 方法。
public class School implements Cloneable{
private String schoolName;
// 省略构造函数、Getter&Setter方法
@Override
protected Object clone() throws CloneNotSupportedException {
try {
return (School)super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Student implements Cloneable{
private School school;
// 省略构造函数、Getter&Setter方法
@Override
protected Object clone() throws CloneNotSupportedException {
try {
Student student = (Student) super.clone();
return student;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试 :
Student student1 = new Student(new School("第一中学"));
Student student2 = (Student) student1.clone();
//true
System.out.println(student1.getSchool() == student2.getSchool());
从输出结果就可以看出, student1 的克隆对象和 student1 使用的仍然是同一个 School 对象。
深拷贝
这里我们简单对 Student类的 clone() 方法进行修改,连带着要把 Student 对象内部的 School 对象一起复制。
@Override
protected Object clone() throws CloneNotSupportedException {
try {
Student student = (Student) super.clone();
student.setSchool((School) student.getSchool().clone());
return student;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
测试 :
Student student1 = new Student(new School("第一中学"));
Student student2 = (Student) student1.clone();
//false
System.out.println(student1.getSchool() == student2.getSchool());
从输出结果就可以看出,student1 的克隆对象和 student1 包含的 School 对象已经是不同的了。
那什么是引用拷贝呢?
简单来说,引用拷贝就是两个不同的引用指向同一个对象。
什么是不可变对象?好处是什么?
不可变对象指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象,如 String、Integer及其它包装类.不可变对象最大的好处是线程安全。
能否创建一个包含可变对象的不可变对象?
当然可以,比如final Person[] persons = new Persion[]{}。persons是不可变对象的引用,但其数组中的Person实例却是可变的。这种情况下需要特别谨慎,不要共享可变对象的引用。这种情况下,如果数据需要变化时,就返回原对象的一个拷贝.
值传递和引用传递的区别的什么?为什么说Java中只有值传递?
值传递(pass by value)是指在调用方法时将实参复制一份传递到方法中,这样当方法对形参进行修改时不会影响到实参。
引用传递(pass by reference)是指在调用方法时将实参的地址直接传递到方法中,那么在方法中对形参所进行的修改,将影响到实参。
值传递和引用传递的关键区别有两点:
-
调用方法时有没有对实参进行复制。
-
方法内对形参的修改会不会影响到实参。
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。Java 的确是值传递的。只不过,引用类型在调用有参方法的时候,传递的是对象的引用,并不是对象本身。而对象的引用在传递的过程中并没有发生改变,虽然对象本身发生了变化。
如果想要详细了解这个问题,可以参考这篇文章——Java到底是值传递还是引用传递?
对象相等判断
== 和 equals 区别是什么?
==常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较;
如果 == 比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;
如果 == 是比较的两个对象,那么比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域;
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象;
看一看Object类中equals方法的源码:
public boolean equals(Object obj) {
return (this == obj);
}
它的作用也是判断两个对象是否相等,般有两种使用情况:
-
情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
-
情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。
Java语言规范要求equals方法具有以下特性:
- 自反性。对于任意不为null的引用值x,x.equals(x)一定是true。
- 对称性)。对于任意不为null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true。
- 传递性。对于任意不为null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那么x.equals(z)一定是true。
- 一致性。对于任意不为null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false。
- 对于任意不为null的引用值x,x.equals(null)返回false。
介绍下hashCode()?
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode?
以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。
但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
equals方法和hashcode的关系?
-
如果两个对象equals相等,那么这两个对象的HashCode一定也相同;
-
如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置。hashCode()只表示对象的哈希码,哈希码相同的对象不一定相等,反之,没有重写equals方法的前提下,两个对象相等,则hashCode一定相同。
-
两个对象相等,对两个对象分别调用equals方法都返回true;
为什么重写 equals 方法必须重写 hashcode 方法 ?
判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。
如果想要详细了解这个问题,可以参考这篇文章——HashCode详解。