去年去阿里面试面试官居然问我Java类和对象我是这样回答的

18次阅读

共计 5491 个字符,预计需要花费 14 分钟才能阅读完成。

1. 谈谈你对 Java 面向对象的理解?

  面向对象就是把构成问题的事务分解成一个个对象,建立对象的目的不是一个步骤,而是为了描述一个事务在解决问题中的行为。类是面向对象的一个重要概念,类是很多个具有相同属性和行为特征的对象抽象出来的,对象是类的一个实例。

 类具有三个属性:继承、封装和多态。

封装:通过把程序模块化、对象化,通过把这些具体事物的特性属性和通过这些属性实现的具体方法放到一个类中。核心思想就是“隐藏细节”,“数据安全”。利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能的隐藏内部的细节,只保留一些对外的接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流交互。

继承:子类可以继承父类的属性和方法,当需要新的属性和方法时,可以对其进行拓展。

多态:子类在继承父类的方法和属性的同时,当需要实现同一事务,表现出不同的行为特征,此时需要面向对象的另一特性,多态。需要在子类中把父类中实现某一事物的行为特征重新实现一遍,多态包含了重写和重载。重写就是把子类从父类那里继承的方法重新写一遍,这样父类里相同的方法就被覆盖了,还可以通过 super. 方法调用父类的方法;重载是类中方法名相同,参数不同的情况,可以使形参类型不同,形参个数不同或者形参顺序不同,但是返回值类型必须相同。还可以通过继承的上下转型,接口的回调来实现。

2.Java 面向对象的特征?

  封装、继承和多态。

3. 谈谈你对 String、StringBuffer、StringBuilder 的理解?

3.1String 类使用“+”进行字符串拼接操作

(1)字符串常量 + 字符串常量

编译器直接将两个常量优化为一个字符串常量

(2)字符串变量 + 字符串常量,解析一下步骤:

a.StringBuilder s=new StringBuilder();

b.s.append(“ 母 ”);

c.s.append(“ 亲 ”);

d.String str=S.toString();

3.2String

(1). String 是不可变的,它每次的字符串拼接都是 new 一个新的 String 进行接收。final 修饰了底层的字符数组,因此内容不可变。jdk8 是 char[]数组,jdk9 中改成了 byte[]数组和一个标示编码的 coder(utf16)(因为 char[]占两个字节,byte 占 1 个字节)

(2).String 类的运行创建机理

   由于 String 在 Java 世界中使用过于频繁,Java 为了避免在一个系统中使用大量的 Java 对象,引入了字符串常量池的概念。

其运行机制是:创建一个字符串的时候,首先检查池中是否有相等的字符串对象,如果有就不需要创建,直接从池中找到对象引用。如果没有的话,新建字符串对象,返回对象引用。但是通过 new 方法创建的不会检查常量池中是否存在,而是在堆或栈中重新创建一个对象,也不会把对象放在常量池中,上面的情况只适用于直接给 String 引用赋值的情况。

注意:jdk6 中 String 是 immutable Strng 提供了 inter()方法可以将 Strng 对象添加到池中,并且返回该对象的引用。(如果由 equals()确定池中有该字符串,那就直接返回)。

 在 Java6 中,在 String 类中提供了 intern() 方法(不推荐使用)。它实现的功能:如果缓存里有字符串,就返回缓存里的实例,如果没有,就把它加到缓存里。

 被缓存的字符串存在于 PermGen(永久代)里,这里空间很小,并且基本只有 FullGC 这样的垃圾收集器来光顾。所以很容易 OOM 错误。所以在后来,就被放到了 堆 中。设置永久代在 Java 8 中被元数据区替代了。

当两个 String 对象拥有相等的值的时候,他们只引用字符串中同一个拷贝。当同一个字符串大量出现的时候,可以大量节省内存空间。

3.3StringBuilder 和 StringBuffer

StringBuilder 和 StringBuffer 底层都是利用了可修改的数组,都集成了 AbstractStringBuilder, 里面包含了基本操作。区别在于 StringBuffer 添加了 synchronized.。

3.4String 和 StringBuilder、StringBuffer 的比较

3.4.1 运行速度

StringBuilder>StringBuffer>String

StringBuilder 和 StringBuffer 的对象是变量,对变量进行操作就是直接对对象进行更改,而不进行创建和回收的操作,String 类针对字符串拼接过程,变量 + 字符串常量。底层操作参考字符串的拼接过程,所以 String 最慢。

3.4.2 线程安全

  StringBuilder 是线程不安全的,而 StringBuffer 是线程安全的,StringBuffer 带有 Synchronized 关键字,所以可以保证线程是安全的。StringBuilder 的方法没有该关键字,所以不能保证线程安全,有可能会出现不安全的操作。

总之:

String:适用于少量的字符串操作的情况。

StringBuffer:适用于多线程下在字符缓冲区进行大量操作的情况。

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况。

4. 谈谈你对 Synchronized 关键字的理解?

  Synchronized 关键字解决的是多个线程之间访问资源的同步性。

4.1 Synchronized 的三种使用方法:

(1)修饰实例方法,作用于当前对象实例加锁,进入同步代码块前需要获取当前对象实例的锁。

(2)修饰静态方法,作用于当前类对象的锁,进入同步代码块前需要获取当前类对象的锁。也就是给当前类加锁,会作用于当前类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static 表示这是该类的一个静态资源,不管 new 了多少个对象,都只有一份,所以对该类的所有对象都加了锁), 所以如果线程 A 调用了一个实例对象的非静态 Synchronized 方法,而线程 B 调用了该实例对象所属类的静态 Synchronized 方法,不会发生互斥,因为访问静态 Synchronized 方法,占用的是当前类的锁,而访问非静态 Synchronized 占用的是当前实例对象的锁。

(3)修饰代码块,指定加锁对象,在进入同步代码块前需要获取给定对象的锁。

Synchronized 是同步锁:

(1)修饰一个代码块,被修饰的代码块称为同步语句块,作用的范围是整个代码块内部,作用的对象是调用整个代码块的对象。

(2)修饰一个方法,被修饰的方法是同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。

(3)修饰一个静态方法,其作用的范围是整个静态方法,作用的对象是整个类的所有对象。

(4)修饰一个类,其作用的范围是整个类内部,作用的对象是整个类的所有对象。

4.2Synchronized 的原理

实现原理:JVM 是通过进入、退出对象监视器(Monitor)来实现对方法、同步块同步的,而对象监视器(Monitor)的实现依赖于底层操作系统的互斥锁(Mutex Lock)实现。具体实现是在编译之后再同步方法调用前加入一个 monitor.enter 指令。在退出方法和异常前插入 monitor.exit 指令。对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。当执行 monitor.enter 指令时,线程试图获取 monitor 的持有权,当计数器为 0 时可以成功获取,相应的锁计数器置为 1 即加 1,当执行 monitor.exit 时,锁计数器置为 0,表明锁被释放,当线程获取对象锁失败时,线程就要进入阻塞等待,直到其他线程释放锁为止。

对 Synchronized 修饰方法时,添加一个 ACC.SYNCHRONIZED 来标识,该标识指明了该方法是一个同步方法。JVM 通过该标示来判断一个方法是否是同步方法,从而执行相应的同步调用。

4.3Synchronized 的优化

  Synchronized 是一种重量级锁,会涉及到操作系统状态的切换。影响效率。jdk1.6 中进行了优化,为了减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。(后面介绍)

5. 谈谈你对单例模式的理解?

Java 中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例,饿汉式单例、登记式单例三种。

5.1. 单例模式有一下特点:

(1)单例类只能有一个实例。

(2)单例类只能自己创建自己的唯一实例。

(3)单例类必须给所有其他对象提供这一实例。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志、对象、对话框、打印机、显卡的驱动程序常被设计成单例模式。singleton 通过将构造方法限定为 private 避免了类在外部被实例化。在同一虚拟机范围内,singleton 的唯一实例只能通过 getlnstance()方法访问。(事实上,通过 java 反射机制是能够实例化构造方法为 private 的类的,但是基本会使所有的 Java 单例模式失效,姑且认为反射机制不存在)

5.2. 时间和空间

比较以上两种方法,懒汉模式是典型的时间换空间,每次在获取实例前,都会进行判断看是否需要进行创建实例,浪费时间,但是如果如果一直不使用的话,则不会创建实例,将诶月内存空间。

饿汉模式是典型的空间换时间,在类装载的时候方法调用之前就已经创建出来,不管你用不用,先创建出来,每次调用的时候就不需要再判断了,节省了时间。

5.3. 线程安全

(1)不加 Synchronized 的懒汉模式是不安全的,例如线程 A 在创建实例时,首先判断是否需要创建实例,判断为 true, 创建实例,在还为创建完成时,线程 B 也进入了创建实例,判断结果也为 true, 此时也进入了创建实例中,此时创建出了两个实例,则单例模式失效。

(2)饿汉模式是线程安全的,因为虚拟机只会装载一次,在装载类的时候是不会发生并发的。

5.4. 单例模式的优点

Java 单例模式的作用是保证在 java 应用程序中,一个 class 只有一个实例存在,节省了内存空间,因为它限制了实例个数,有利于 Java 垃圾回收。

5.5. 单例模式的应用

   饿汉模式加载类的时候比较慢,但是运行时获取对象的速度比较快,从加载到应用结束会一直占用资源。

   懒汉模式运行时获取对象的速度比较慢,但是加载类的速度比较快,它的整个应用的的生命周期只有一部分在占用资源。

这两种模式对于初始化较快,占用资源较少的轻量级对象没有较大差异。

对于初始化较慢,占用资源较多的重量级对象从用户角度分析选择饿汉模式比较好。因为虽然初始化的速度叫较慢,但是运行速度较快。

   数据连接池应用单例模式是比较好的选择,可以屏蔽不同数据库之间的差异,降低系统对数据库的耦合性,可以对多个系统使用,提高复用性,提高数据库连接的管理。数据连接池是重量级对象,一个应用只需保存一份就好,节省资源,方便管理。

6. 谈谈你对 final 关键字的理解

1. 修饰变量

凡是对成员变量或者局部变量 (在方法) 声明 final 的变量都是 final 变量,final 变量通常和 static 关键字一起使用,做为常量。

final 修饰基本数据类型时,基本数据类型必须赋初值且不能改变,修饰引用变量时,该引用变量不能再指向其他对象。

2. 修饰方法

final 也可以修饰方法,方法前面 final 关键字,代表这个方法不可以被子类的方法重写。如果一个方法的功能已经足够完整。子类不需要改变的话,可以声明该方法是 final。final 方法比非 final 要快,因为在编译的时候已经静态绑定了。不需要在运行的时候动态绑定了。

3. 修饰类

final 修饰的类叫做 final 类,final 类的功能通常是完整的,他们不能被继承,Java 中有许多类是 final,例如 String、Integer 以及其它包装类。

.4. 深入理解 java 关键字

public final class Day4Test {
        int i=5;
        public int adds(int i){return i;}
    public static void main(String[] args) {System.out.println(new Day4Test().adds(6));
    }
}
运行结果:6

(2)static 修饰变量时,在类加载时,该变量就已经被初始化,不会对象创建再次被加载,当变量被 static 修饰时就表示该变量只能被初始化一次。

public  class Day4Test {public final double s = Math.random();
    public static double j = Math.random();
 
    public static void main(String[] args) {Day4Test data1 = new Day4Test();
        Day4Test data2 = new Day4Test();
        System.out.println(data1.s);
        System.out.println(data2.s);
        System.out.println(data1.j);
        System.out.println(data2.j);
    }
}
运行结果:0.5351276053333238
0.8784147384393909
0.8476665776623683
0.8476665776623683

static 修饰的变量 j 被创建了两次,但是只初始化了一次,只没有被改变。

正文完
 0