Java基础知识面试
1. 面向对象的理解[程序设计思想]
计算机的背景介绍
- 在早期的计算机主要是实现算术运算的,例如:1+1+2=4等计算问题,所以前期计算机都是面向过程编程的,以实现某种复杂计算功能为主。
- 60年代中期,大容量、高速度计算机的出现,使计算机的应用范围迅zhi速扩大,软件开发急剧增长。随着计算机技术的不断提高,计算机被用于解决越来越复杂的问题。面向过程编程的思想不再支持实现该复杂的功能了,因此高级语言开始出现。
- 软件系统的规模越来越大,复杂程度越来越高,软件可靠性问题也越来越突出。原来的个人设计、个人使用的方式不再能满足要求,迫切需要改变软件生产方式,提高软件生产率,软件危机开始爆发 。70年代,即使科学家总结出多种开发方法【如:结构化方法】,但依然满足不了市场的需求。
- 20世纪60年代挪威计算中心发布的simula语言,首次引入了类的概念和继承机制,该语言的诞生是面向对象发展历史上的第一个里程碑。面向对象程序设计在软件开发领域引起了大的变革,极大地提高了软件开发的效率,为解决软件危机带来了一线生机。因此面向对象编程思想日益成熟,并在80年代[1972年],正式使用了面向对象的这个术语。
- 面向对象编程思想的繁荣阶段。从20世纪80年代中期到90年代,是面向对象语言走向繁荣的阶段。其主要表现是大批比较实用的面向对象编程语言的涌现,如 C++。
- 当前状况: 面向对象方法几乎覆盖了计算机软件领域的所有分支。例如,已经出现了面向对象的编程语言、面向对象的分析、面向对象的设计、面向对象的测试、面向对象的维护、面向对象的图形用户界面、面向对象的数据库、面向对象的数据结构…
参考网址:https://baike.baidu.com/item/simula/6913723?fr=aladdin
1.1 面向过程[ PO(Procedure Oriented) ]
特点:
- 面向过程是一种以过程为中心的编程思想。
- 分析出解决问题的所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候依次调用即可。
1.2 面向对象[ OO(Object Oriented) ]
-
面向对象,是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
-
面向对象是一种软件开发方法。面向对象的概念已经超越了程序设计和软件开发,面向对象的应用包括了数据库系统、交互式界面、应用结构、分布式系统、网络管理结构、CAD技术、人工智能等领域。
- 面向对象,是一种对现实世界理解和抽象的方法。
- 面向对象的思想是,将现实世界中的实体或事物,抽象成对象,分析其属性和行为。
- 在OOP中,现实世界的所有事物全部都被视为对象,
1.3 总结
面向过程: 是一种以过程为中心的编程思想。分析出解决问题的所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候依次调用即可。
面向对象: 是把构成问题事务分解或抽象成对象,分析其属性和行为,并建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
2. static关键字解析
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。
1.1 static关键字的用途
static方法:
static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
class MyObject{
private static String str1 = "staticProperty" ;
private String str2 = "property" ;
public MyObject(){}
public void print1(){
System.out.println(str1) ; // 正常
System.out.println(str2) ; // 正常
print2() ; // 正常
}
public static void print2(){
System.out.println(str1) ; // 正常
System.out.println(str2) ; // 静态方法,不能访问 非静态变量
print1() ; // 静态方法,不能访问 非静态方法
}
}
static变量:
static变量也称作静态变量。静态变量和非静态变量的区别是:
- 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
- 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
static代码块:
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
class Person{
private Date birthDate;
public Person(Date birthDate) {
this.birthDate = birthDate;
}
boolean isBornBoomer() {
Date startDate = Date.valueOf("1946");
Date endDate = Date.valueOf("1964");
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
1.2 static关键字的误区
1.2.1 static关键字会改变类中成员的访问权限吗?
public class Test{
public static void main(String[] args){
System.out.println(Person.name) ; // 正常使用
System.out.println(Person.age) ; // 错误使用
}
}
class Person{
public static String name = "张三" ;
private static int age = 21 ;
}
结论: static关键字并不会改变变量和方法的访问权限。
1.2.2 能通过this访问静态成员变量吗?
public class Main {
static int value = 33;
public static void main(String[] args) throws Exception{
new Main().printValue();
}
private void printValue(){
int value = 3;
System.out.println(this.value); // 程序编译时出错,但运行时正常,显示结果 33
}
}
总结: this和static的理解
- this代表当前对象,那么通过new Test()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。
- 静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
1.3 常见的笔试面试题
(1) 下面这段代码的输出结果是什么?
public class Test extends Base{
static{
System.out.println("test【Child】 static");
}
{
System.out.println("Test【Child】 Black");
}
public Test(){
System.out.println("test【Child】 constructor");
}
public static void main(String[] args) {
new Test();
}
}
class Base{
static{
System.out.println("base【parent】 static");
}
{
System.out.println("Base【parent】 Black");
}
public Base(){
System.out.println("base【parent】 constructor");
}
}
运行结果:
base【parent】 static
test【Child】 static
Base【parent】 Black
base【parent】 constructor
Test【Child】 Black
test【Child】 constructor
结论:
- 加载顺序:【父】静态代码块>【子】静态代码块>【父】代码块>【父】构造方法>【子】代码块>【子】构造方法
- 程序的执行开始:
- 再执行Test类的main()方法。
- 必须先加载Test类【执行main方法执行】
- 在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类
- 在加载Base类的时候,发现有static块,便执行了static块。
- 在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块
- 在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。
(2) 这段代码的输出结果是什么?
/**
* 描述:<br> 验证 类加载的过程
* 验证对象: 1. 静态代码【】快
* 2. 代码【块】
* 3. 构造方法
* </>
* @author 周志通
* @date 2020/7/31 23:07
**/
public class Test {
Person person = new Person("3. Test"); // 3 相当于代码块
static{ // 静态代码块
System.out.println("1. Test static"); // 1
}
public Test() {
System.out.println("4. Test constructor"); // 4
}
public static void main(String[] args) {
new MyClass();
}
}
class Person{
static{
System.out.println("3.1 Person static"); // 3.1
}
public Person(String str) {
System.out.println(str + " person "); // 3.2
}
}
class MyClass extends Test {
Person person = new Person("5. MyClass"); // 5
static{
System.out.println("2. MyClass static"); // 2
}
public MyClass() {
System.out.println("6. MyClass constructor"); // 6
}
}
1. Test static
2. MyClass static
3.1 Person static
3. Test person
4. Test constructor
5. MyClass person
6. MyClass constructor
总结:
- 代码的具体执行过程:
- 首先加载Test类。
- 接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。
- 在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。
- 在加载完之后,就通过构造器来生成对象。
- 而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。
参考网址1:https://www.cnblogs.com/shanqiang1/p/10786804.html
参考网址2:http://www.cnblogs.com/dolphin0520/p/3799052.html
3. java的序列化机制
3.1 序列化是什么?
- Java序列化:是指把Java对象数据信息转换为字节序列,并将该对象的信息写入到IO流中的过程,或通过网络传输,成为Java序列化过程。
- 反序列化: 是指从IO流中的对象,恢复成原来对象的样子。
使用案例:
/**
* 描述:<br> 初步了解序列化机制和反序列化机制
* 过程: 1. User1实现了序列化接口 Serializable
* 2. 通过 ObjectOutputStream 对象实现将User1对象的信息序列化,并存入本地磁盘文件中
* 3. 最后通过 ObjectInputStream 对象实现将本地磁盘文件的信息,反序列化成 User1 对象中。
*
* 结果: 1. 实现序列化接口 Serializable 的类,在写入IO流后,能成功反序列化成相关对象信息;
* 而没有实现序列化接口的类,则不能反序列化成相关对象,
* 并会出现 java.io.NotSerializableException 相关错误信息
* </>
* @author 周志通
* @date 2020/8/2 21:25
**/
public class SerialDemo01 {
public static void main(String[] args) {
serializeUser1();
deserializationUser1();
}
// 1. 序列化
private static void serializeUser1(){
User1 user1 = new User1(1,"张三---English") ;
try {
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("D:/java_test/user1.txt")) ;
oos.writeObject(user1);
oos.close();
}catch (Exception e){
}
}
// 2. 反序列化
private static void deserializationUser1(){
try {
File file = new File("D:/java_test/user1.txt") ;
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)) ;
System.out.println("开始反序列化:");
User1 user1 =(User1) ois.readObject() ;
System.out.println("反序列化结果:"+user1);
}catch (Exception e){
e.printStackTrack() ;
}
}
}
class User1 implements Serializable{
private static final long serialVersionUID = 1L ;
private Integer age ;
private String name ;
// setter、getter、toString()
}
-
序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
-
使用场景:所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口。
3.2 序列化实现方式
两种方式:
- 实现
Serializable
接口 - 实现
Externalizable
接口
注意:
- 'serialVersionUID':验证 版本号【`在JVM底层实现`】。当不同时,会出现异常信息:InvalidCastException
1. Serializable
接口
- Serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
使用案例:
class Person implements Serializable {
private static final long serialVersionUID = 1L ;
private String name ;
private Integer age ;
// setter、getter
}
1.1 普通序列化
步骤:
- 步骤一:创建一个ObjectOutputStream 输出流;
- 步骤二:调用 ObjectOutputStream 对象的writeObject()方法输出序列化对象。
(详情使用见👆上文对序列化介绍部分的内容)
1.2 反序列化
步骤:
- 步骤一:创建一个ObjectInputStream 输入流;
- 步骤二:调用 ObjectInputStream 对象的readObject()方法得到序列化后的对象【需要强制转换】。
(详情使用见👆上文对序列化介绍部分的内容)
注意:
- 反序列化并不会调用构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成。
1.3 反序列化
====
2. 实现Externalizable
接口:强制自定义序列化
使用案例:
public class Person implements Externalizable {
private static final long serialVersionUID = 1L;
private transient Long id;
private String name;
private transient Integer age;
private String remarks;
// setter、getter
public Person() {
System.out.println("构造器");
this.age = 23;
}
public Person(Long id, String name, String remarks) {
this.id = id;
this.name = name;
this.age = age;
this.remarks = remarks;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(id);
out.writeObject(name);
out.writeObject(age);
out.writeObject(remarks);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 要与 writeExternal 写入的顺序一致
id = (Long) in.readObject();
name = (String) in.readObject();
age = (Integer) in.readObject();
remarks = (String) in.readObject();
}
}
public class Client {
private static String filePath = "C:\\Users\\Admin\\Desktop\\test.txt";
public static void main(String[] args) throws IOException, ClassNotFoundException {
externalizable();
antiExternalizable();
}
// 2.1 序列化 Externalizable
private static void externalizable() throws IOException {
Person person = new Person(1L, "张三", "第一个测试用例");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filePath));
out.writeObject(person);
out.close();
}
// 2.1 序列化 Externalizable
private static void antiExternalizable() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream(filePath);
ObjectInputStream stream = new ObjectInputStream(fileInputStream);
Person person = (Person) stream.readObject();
System.out.println(person);
}
}
总结:
- 1. 关键字:`transient`,在此处没有任何效果。
- 2. 通过无参构造器创建对象,因此 对象中,必须存在一个无参构造
- 3. 'writeExternal'、'readExternal' 分别实现 序列化操作和反序列化操作。
3.3 深入总结:
1. Java序列化算法
- 所有保存到磁盘的对象都有一个序列化编码号
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
2. Java序列化算法潜在的问题
参考网址:Java序列化详解