目录
- 1.面向对象和面向过程有什么区别?
- 2.面向对象的有哪些特征?
- 3.静态变量和实例变量有什么区别?
- 4.Java 对象实例化顺序是怎样的?
- 5.浅拷贝和深拷贝的区别是什么?
- 5.1.浅拷贝
- 5.2.深拷贝
- 5.3.总结
- 6.Java 中创建对象的方式有哪几种?
- 7.重写和重载的区别是什么?
- 8.Java 的四种引用方式分别是什么?
- 9.构造方法有哪些特点?
- 10.抽象类 (abstract class) 和接口 (interface) 有什么共同点和区别?
- 11.什么是内部类?为什么需要使用内部类?
- 12.匿名内部类可以继承类或实现接口吗?为什么?
- 13.说说内存中的栈 (stack)、堆 (heap) 和静态存储区的用法。
1.面向对象和面向过程有什么区别?
(1)面向过程: 是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发。
(2)面向对象: 是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特征,所以易维护、易复用、易扩展,可以设计出低耦合的系统。
2.面向对象的有哪些特征?
(1)抽象
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
(2)继承
继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类或基类);得到继承信息的类被称为子类(或派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
(3)封装
通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好,因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
(4)多态
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单地说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电, 甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。
① 方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override) 实现的是运行时的多态性(也称为后绑定)。
② 运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1)方法重写。即子类继承父类并重写父类中已 有的或抽象的方法;2)对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
3.静态变量和实例变量有什么区别?
(1)静态变量是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一份。静态变量可以实现让多个对象共享内存。在 Java 开发中,上下文类和工具类中通常会有大量的静态成员。
(2)实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。
4.Java 对象实例化顺序是怎样的?
(1)类加载器实例化时进行的操作步骤:加载 -> 连接 -> 初始化。
(2)实例化优先级顺序:父类 > 子类 , 静态代码块 > 非静态代码块 > 构造函数,具体按照如下顺序:
① 按照代码书写顺序加载父类静态变量和父类静态代码块;
② 按照代码书写顺序加载子类静态变量和子类静态代码块;
③ 父类非静态变量(父类实例成员变量);
④ 父类非静态代码块;
⑤ 父类构造函数;
⑥ 子类非静态变量(子类实例成员变量);
⑦ 子类非静态代码块;
⑧ 子类构造函数;
(3)举例说明如下:
class A {
//静态代码块(只初始化一次)
static {
System.out.println("父类的静态代码块...");
}
//非静态代码块
{
System.out.println("父类的非静态代码块...");
}
//构造函数
public A() {
System.out.println("父类的构造函数...");
}
}
class B extends A {
//静态代码块(只初始化一次)
static {
System.out.println("子类的静态代码块...");
}
//非静态代码块
{
System.out.println("子类的非静态代码块...");
}
//构造函数
public B() {
System.out.println("子类的构造函数...");
}
}
public class Solution {
public static void main(String[] args) {
A ab = new B();
System.out.println("---------");
ab = new B();
}
}
上述代码的输出结果如下:
父类的静态代码块...
子类的静态代码块...
父类的非静态代码块...
父类的构造函数...
子类的非静态代码块...
子类的构造函数...
------------
父类的非静态代码块...
父类的构造函数...
子类的非静态代码块...
子类的构造函数...
5.浅拷贝和深拷贝的区别是什么?
5.1.浅拷贝
浅拷贝是创建一个新对象,该对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,那么拷贝的就是内存地址 。
public class Address {
int id;
String addressName;
public int getId() {
return id;
}
public String getAddressName() {
return addressName;
}
public Address(int id, String addressName) {
this.id = id;
this.addressName = addressName;
}
public void setId(int id) {
this.id = id;
}
public void setAddressName(String addressName) {
this.addressName = addressName;
}
@Override
public String toString() {
return "Address{" +
"id=" + id +
", addressName='" + addressName + '\'' +
'}';
}
}
public class Person implements Cloneable {
private int id;
private String name;
private Address address;
public String getName() {
return name;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Person(int id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", address=" + address +
'}';
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(1, "小华", new Address(1, "北京"));
//浅拷贝
Person person2 = (Person) person1.clone();
System.out.println("person1 == person2 的结果为:" + (person1 == person2));
System.out.println("\n改变person1的属性值之前:");
System.out.println("person1:" + person1);
System.out.println("person2:" + person2);
System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
person1.setId(2);
person1.setName("小明");
person1.getAddress().setId(2);
person1.getAddress().setAddressName("武汉");
System.out.println("\n改变person1的属性值之后:");
System.out.println("person1:" + person1);
System.out.println("person2:" + person2);
System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
}
}
5.2.深拷贝
(1)深拷贝是将一个对象从内存中完整的拷贝一份出来,即从堆内存中开辟一个新的区域存放新对象。新对象与原对象不共享内存,修改新对象也不会影响原对象。
(2)实现方式如下:
① 通过对象序列化实现深拷贝(推荐)
先将 Address 类和 Person 类实现 Serializable 接口,然后再修改Test.java中的代码即可。
public class Test {
public static void main(String[] args) throws Exception {
Person person1 = new Person(1,"小华",new Address(1,"北京"));
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\testData\\a.txt"));
//写对象
oos.writeObject(person1);
//释放资源
oos.close();
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\testData\\a.txt"));
//读取对象(深拷贝)
Person person2 = (Person) ois.readObject();
//释放资源
ois.close();
System.out.println("person1 == person2 的结果为:" + (person1 == person2));
System.out.println("\n改变person1的属性值之前:");
System.out.println("person1:" + person1);
System.out.println("person2:" + person2);
System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
person1.setId(2);
person1.setName("小明");
person1.getAddress().setId(2);
person1.getAddress().setAddressName("武汉");
System.out.println("\n改变person1的属性值之后:");
System.out.println("person1:" + person1);
System.out.println("person2:" + person2);
System.out.println("person1.getName() == person2.getName()的结果为:" + (person1.getName() == person2.getName()));
System.out.println("person1.getAddress() == person2.getAddress()的结果为:" + (person1.getAddress() == person2.getAddress()));
}
}
② 重写Person 类中的 clone() 方法来实现深拷贝
public class Address implements Cloneable {
//...
@Override
public Object clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
//...
}
public class Person implements Cloneable {
//...
@Override
public Object clone() throws CloneNotSupportedException {
Person person = null;
person = (Person)super.clone();
//对引用数据类型单独处理
person.name = new String(name);
person.address = (Address)address.clone();
return person;
}
//...
}
5.3.总结
操作 | 是否指向同一个堆内存地址 | 基本数据类型 | 引用数据类型 |
---|---|---|---|
赋值 | 是 | 会改变 | 会改变 |
浅拷贝 | 否 | 不会改变 | 会改变 |
深拷贝 | 否 | 不会改变 | 不会改变 |
6.Java 中创建对象的方式有哪几种?
(1)使用 new 关键字
这是最常见也是最简单的创建对象的方式,通过这种方式,我们可以调用任意的构造函数(无参的和有参的)。
public class Student {
public static void main(String[] args) {
Student student = new Student();
}
}
(2)通过反射机制
① 使用 Class 类中的 newInstance 方法
使用 Class 类中的 newInstance 方法创建对象。这个 newInstance 方法调用无参的构造函数创建对象。
public class Student {
public static void main(String[] args) throws Exception {
//使用 Class 类中的 newInstance 方法创建对象,有以下两种方式
Student student1 = (Student) Class.forName("test.Student").newInstance();
Student student2 = Student.class.newInstance();
System.out.println(student1 == student2); //false
}
}
② Constructor 类中的 newInstance 方法
和 Class 类中的 newInstance 方法很像, java.lang.reflect.Constructor 类里也有一个 newInstance 方法可以创建对象。我们可以通过这个newInstance 方法调用有参数的和私有的构造函数。
public class Student {
public static void main(String[] args) throws Exception {
Constructor<Student> constructor = Student.class.getConstructor();
Student student = constructor.newInstance();
}
}
(3)使用 clone 方法
无论何时我们调用一个对象的 clone(),JVM 就会创建一个新的对象,将前面对象的内容全部拷贝进去。用 clone() 创建对象并不会调用任何构造函数。要使用 clone(),我们需要先实现 Cloneable 接口并实现其定义的 clone()。
public class Student implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Student)super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student();
Student student2 = (Student)student1.clone();
System.out.println(student1 == student2); //false
}
}
(4)通过序列化机制
① 序列化就是把对象通过流的方式存储到文件中,此对象要重写 Serializable 接口才能被序列化。
② 反序列化就是把文件中的内容读取出来,还原为 Java 对象,该过程也需要实现 Serializable 接口。
当我们序列化和反序列化一个对象,JVM 会给我们创建一个单独的对象。在反序列化时,JVM 创建对象并不会调用任何构造函数。
public class Student implements Serializable {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("stu.txt"));
Student student1 = new Student();
student1.setAge(18);
out.writeObject(student1);
ObjectInputStream in = new ObjectInputStream(new FileInputStream("stu.txt"));
Student student2 = (Student) in.readObject();
System.out.println(student2.getAge()); //18
System.out.println(student1 == student2); //false
}
}
7.重写和重载的区别是什么?
(1)重写 (Override)
从字面上看,重写就是重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法, 此时可以对方法体进行修改或重写。但需要注意以下几点,简称两同两小加一大原则:
① 方法名、参数列表相同;
② 子类返回类型小于等于父类方法返回类型、重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常;
③ 子类函数的访问修饰权限要大于等于父类的 (public > protected > default > private);
public class Father {
public void sayHello() {
System.out.println("Hello, Father"); //Hello, Son
}
public static void main(String[] args) {
Son s = new Son();
s.sayHello();
}
}
class Son extends Father{
@Override
public void sayHello() {
// TODO Auto-generated method stub
System.out.println("Hello, Son");
}
}
(2)重载 (Overload)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载,它是一个类中多态性的一种表现。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
8.Java 的四种引用方式分别是什么?
(1)强引用 (Strong Reference)
强引用是平常中使用最多的引用,在程序内存不足时也不会被垃圾收集器回收。
//使用方式
Object obj=new Object()
(2)软引用 (Soft Reference)
① 软引用在程序内存不足时会被回收。
② 可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。
/*
注意:wrf 这个引用也是强引用,它是指向 SoftReference 这个对象的,
这里的软引用指的是指向new String("str")的引用,也就是 SoftReference 类中T
*/
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
(3)弱引用 (Weak Reference)
① 弱引用就是只要 JVM 垃圾回收器发现了它,就会将其回收。
② 可用场景: Java 源码中 java.util.WeakHashMap 中的 key 就是使用弱引用,一旦我不需要某个引用,JVM 会自动帮忙处理它,这样我们就不需要做其它操作。
//使用方式
WeakReference<String> wrf = new WeakReference<String>(str);
(4)虚引用 (Phantom Reference)
① 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。需要注意的是,其它引用是被 JVM 回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。此外,虚引用创建的时候,必须带有ReferenceQueue。
② 可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效。
//使用方式
PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
注意:上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用。
9.构造方法有哪些特点?
(1)名字必须与类名相同。
(2)没有返回值,也不能用 void 声明构造函数。
(3)生成类的对象时自动调用,且在整个对象的生命周期内只调用一次。
(4)构造方法不能被 override(重写),但是可以 overload(重载),所以我们经常可以看到一个类中有多个构造方法的情况。
(5)如果用户没有显式定义构造方法,编译器就会默认生成一份构造方法,而且默认生成的构造方法一定是无参的。不过一旦用户定义了一个构造方法,编译器咋不会自动生成的构造方法。
10.抽象类 (abstract class) 和接口 (interface) 有什么共同点和区别?
(1)共同点
- 都不能被实例化;
- 都可以包含抽象方法;
- 都可以有默认实现的方法(JDK 1.8 可以用 default 关键字在接口中定义默认方法);
(2)区别
- 基本使用
- 抽象类只能继承一个,接口可以实现多个,即单继承,多实现;
- 接口比抽象类更加抽象,因为抽象类中可以定义构造方法,可以有抽象方法和具体方法。而接口中不能定义构造方法,而且其中的方法全部都是抽象方法,默认且只能是 public abstract 的;
- 抽象类中可以定义静态块,而接口中则不能;
- 抽象类中可以定义成员变量,并且接口中定义的成员变量实际上都是常量,默认且只能是 public static final 的;
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法;
- 设计目的
- 接口的设计目的是对类的行为进行约束(更准确的说是一种"有"约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
- 抽象类的设计目的是代码复用。当不同的类具有某些相同的行为(记为行为集合 A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了 B,避免让所有的子类来实现 B,这就达到了代码复用的目的。而 A - B 的部分,留给各个子类自己实现。正是因为 A - B 在这里没有实现,所以抽象类不允许实例化出来(否则当调用到 A - B 时,无法执行)。
- 设计思想
- 抽象类是对类本质的抽象(自下而上),表达的是 is xxx 的关系,比如: BMW is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
- 而接口是对行为的抽象(自上而下),表达的是 like xxx 的关系。比如:Bird like a Aircraft(像飞行器一样可以飞),但其本质上 is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
- 使用场景:当关注事物的本质时,使用抽象类;当关注事物的操作时,使用接口。
- 复杂度:抽象类的功能要远超过接口,但是定义抽象类的代价比较高。因为对于Java来说,每个类只能继承一个类,在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度。
11.什么是内部类?为什么需要使用内部类?
(1)简单来说,内部类 (inner class) 是定义在另一个类中的类。
(2)使用内部类的主要原因有以下三点:
① 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
② 内部类可以对同一个包中的其他类隐藏起来。
③ 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷。
12.匿名内部类可以继承类或实现接口吗?为什么?
(1)匿名内部类本质上是对父类方法的重写或对接口方法的实现。从语法角度看,匿名内部类创建处无法使用关键字继承类或实现接口。
(2)原因如下:
① 匿名内部类没有名字,所以它没有构造函数。因为没有构造函数,所以它必须通过父类的构造函数来实例化。即匿名内部类完全把创建对象的任务交给了父类去完成。
② 匿名内部类里创建新的方法没有太大意义,新方法无法被调用。
③ 匿名内部类一般是用来覆盖父类的方法。
④ 匿名内部类没有名字,所以无法进行向下的强制类型转换,只能持有匿名内部类对象引用的变量类型的直接或间接父类。
13.说说内存中的栈 (stack)、堆 (heap) 和静态存储区的用法。
(1)通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;
(2)通过 new 关键字和构造器创建的对象放在堆空间;
(3)程序中的字面量 (literal) 如直接书写的 100、“hello, world” 和常量都是放在静态存储区中。
(4)栈空间操作最快但是也很小,通常大量的对象都是放在堆空间,整个内存包括硬盘上的虚拟内存都可以被当成堆空间来使用。
(5)String str = new String(“hello”);
上面的语句中 str 放在栈中,用 new 创建出来的字符串对象放在堆中,而 “hello” 这个字面量放在静态存储区,一共创建了两个字符串对象。