面试题总结(一)
1. JDK、JRE、JVM的区别和联系
JDK(java程序开发包)=JRE +Tools
JRE=JVM(虚拟机)+API
2. 采用字节码的好处
Java中引入了JVM,即在机器和编译程序之间加了一层抽象的虚拟机器,这台机器在任何平台上都提供给编译程序一个共同的接口。
- 编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码来执行。在Java中,这种供虚拟机理解的代码叫做
字节码(.class)
,它不面向任何特定的处理器,只面向虚拟机
- 每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序通过编译器进行编译后转换为字节码,字节码在虚拟机上执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也解释了Java的编译与解释共存的特点。
Java源代码-->编译器-->jvm可执行的java字节码-->jvm中解释器-->机器可执行的二进制机器码-->程序运行
Java语言采用字节码的方式,一定程度上解决了传统解释型语言执行效率低(运行需要解释环境,速度比编译的要慢,占用资源也要多一些)的问题,同时又保留了解释型语言可移植的特点,所以Java程序运行时很高效,此外,由于字节码不针对一种特定的机器,因此Java源程序无需重新编译即可在不同的计算机上运行,实现一次编译,多次运行。
3. 接口和抽象类的区别
1️⃣ 从语法上来说
-
抽象类可以存在普通成员函数,而接口中只能存在public abstract方法。
-
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
-
抽象类只能继承一个,接口可以实现多个
2️⃣ 从设计目的来说
-
接口是用来对
类的形为进行约束。也就是提供了一种机制,可以强制要求所有的类具有相同的形为,只约束了行为的有无,不限制形为的具体实现。 -
抽象类是为了
代码复用。当不同的类具有相同的行为A,且其中一部分形为B的实现方式一致时,可以让这些类都派生于一个抽象类,这个抽象类中实现了B,避免让所有的子类来实现B,以此来达到代码复用的目的。而A-B的部分,交给各个子类自己实现,正是由于这里A-B的行为没有实现,所以抽象类不允许实例化。
3️⃣ 从本质上来说
-
接口
对行为的抽象,表达的是like a
的关系,比如 Bird like a Aircraft(鸟像飞行器一样可以飞);接口的核心是定义行为,即接口的实现类可以做什么,至于实现类如何实现,主体是谁,接口并不关心。 -
抽象类
对类本质的抽象,表达的是is a
的关系,比如 BaoMa is a Car(宝马是一辆车);抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交给子类去实现。
总结:
-
当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
-
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度
4. 面向对象的四大特性
1️⃣ 抽象
将一类对象的共同特征总结出来构造类的过程
2️⃣ 封装
将过程和数据包围起来,对数据的访问只能通过特定的接口(例如私有变量的get/set
方法)
3️⃣ 继承
从现有类派生出新类的过程
4️⃣ 多态
- 编译时多态:同一方法可根据对象的不同产生不同的效果,也就是
方法的重载
- 运行时多态:父类的引用指向子类对象,一个对象的实际类型确定,但是指向其的引用类型可以有很多
5. 面向对象和面向过程
面向过程(Procedure Oriented)和面向对象(Object Oriented,OO)都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显的显示出来,出现了面向对象思想并成为目前主流的方式。两者都贯穿于软件分析、设计和开发各个阶段,对应面向对象就分别称为面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。
面向对象和面向过程是两种不同的处理问题角度
-
面向过程注重事情的每一步以及顺序
-
面向过程诸众事情有哪些参与者(对象),以及各自需要做什么
比如:洗衣机洗衣服
-
面向过程会将任务拆解成一系列的步骤(函数):
1、打开洗衣机–>2、放衣服–>3、放洗衣粉–>4、清洗–>5、烘干
-
面向对象会拆出人和洗衣机两个对象:
人:打开洗衣机放衣服放洗衣粉
洗衣机:清洗烘干
由此可见,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
面向对象和面向过程的总结
1. 都是解决问题的思维方式,都是代码组织的方式。
2. 解决简单问题可以使用面向过程
3. 解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程。
4. 面向对象具有三大特征:封装性、继承性和多态性,而面向过程没有继承性和多态性,并且面向过程的封装只是封装功能,而面向对象可以封装数据和功能。所以面向对象优势更明显。
6. 静态绑定&动态绑定
在Java方法调用的过程中,JVM是如何知道调用的是哪个类的方法源代码呢?这就涉及到程序绑定,程序绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。
对Java来说,绑定分为静态绑定和动态绑定,或者叫做前期绑定和后期绑定。
1️⃣ 静态绑定
针对Java,可以简单地理解为程序编译期的绑定。
这里特别说明一点,Java当中的方法只有final,static,private和构造方法是静态绑定。
关于final,static,private和构造方法是前期绑定的理解:
对于private的方法,首先一点它不能被继承,既然不能被继承那么就没办法通过它子类的对象来调用,而只能通过这个类自身的对象来调用。因此就可以说private方法和定义这个方法的类绑定在了一起。
final方法虽然可以被继承,但不能被重写(覆盖),虽然子类对象可以调用,但是调用的都是父类中所定义的那个final方法,(由此我们可以知道将方法声明为final类型,一是为了防止方法被覆盖,二是为了有效地关闭java中的动态绑定)。
构造方法也是不能被继承的(网上也有说子类无条件地继承父类的无参数构造函数作为自己的构造函数,不过个人认为这个说法不太恰当,因为我们知道子类是通过super()来调用父类的无参构造方法,来完成对父类的初始化, 而我们使用从父类继承过来的方法是不用这样做的,因此不应该说子类继承了父类的构造方法),因此编译时也可以知道这个构造方法到底是属于哪个类。
对于static方法,具体的原理我也说不太清。不过根据网上的资料和我自己做的实验可以得出结论:static方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)
由上面我们可以得出结论,如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。
2️⃣ 动态绑定
在运行时根据具体对象的类型进行绑定。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体
动态绑定的过程:
-
虚拟机提取对象的实际类型的方法表;
-
虚拟机搜索方法签名
-
调用方法
7. 重载和重写
重写:发生在父子类中,方法名、参数列表必须相同;子类的返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类不能重写该方法。
重载:发生在同一个类中,参数类型不同、个数不同、顺序不同都可以构成重载;
重载方法的返回值可以不同,但是不能仅仅返回值不同,否则编译时报错
重载方法的访问控制符也可以不同,但是不能仅仅访问控制符不同,否则编译时报错
8. Java异常体
Java中的所有异常都来自顶级父类Throwable
,Throwable有两个子类Exception
和Error
-
Error是程序无法处理的错误,一旦出现错误,则程序将被迫停止运行
-
Exception不会导致程序停止,又分为
RunTimeException
和和CheckedException
-
//除0错误:ArithmeticException
//错误的强制类型转换错误:ClassCastException
//数组索引越界:ArrayIndexOutOfBoundsException
//使用了空对象:NullPointerException -
CheckedException常常发生在程序编译过程中,会导致程序编译不通过
例如:打开不存在的文件
9. final关键字
1.作用
-
修饰类:表示类不可被继承
-
修饰方法:表示方法不可被子类覆盖,但是可以重载
-
修饰变量:表示变量一旦被赋值就不可以更改它的值
2.修饰不同变量的区别
1️⃣ 修饰成员变量
-
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
-
如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
2️⃣ 修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)。
3️⃣ 修饰基本数据类型和引用类型数据
- 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改
- 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值可变。
2.为什么局部内部类和匿名内部类只能访问局部final变量
局部内部类或匿名内部类编译之后会产生两个class文件:Test.class
、Test$1.class
,一个是类class,一个是内部类class
局部内部类:
首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡),这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为內部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy".这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类內建立的拷贝保持一致。
10. String、StringBuilder、StringBuffer
-
String底层是final修饰的char[]数组,不可变,每次操作都会产生新的String对象
-
StringBuffer和StringBuilder都是在原对象上操作
-
StringBuffer线程安全(所有方法都用synchronized修饰),StringBuilder线程不安全
性能:StringBuilder>StringBuffer>String
使用场景:经常需要改变字符串内容时使用后面两个,优先使用 StringBuilder,多线程使用共享变量时使用 StringBuffer。
11. 单例模式
暂时未总结,后期更新