Java基础-常见问题

本文最后更新于:2 年前

Java 对象判空?

null:一个对象如果有可能是 null 的话,首先要做的就是判断是否为 null:object == null,否则就有可能会出现空指针异常,这个通常是我们在进行数据库的查询操作时,查询结果首先用 object != null,进行非空判断,然后再进行其他的业务逻辑,这样可以避免出现空指针异常。

isempty:此方法可以使用于字符串,数组,集合都可以用。

plain public boolean isEmpty() {
return value.length == 0;
}
这里是一个对象的长度,使用这个方法,首先要排除对象不为 null,否则当对象为 null 时,调用 isEmpty 方法就会报空指针了。

要想返回 true,也就是一个对象的长度为 0,也就是说首先这个对象肯定不为 null 了,内容为空时,才能返回 true。

当创建一个新的对象时,栈里面有一个对象,堆里面有一个对象,栈里的对象指向堆里面的对象。对象包含引用对象和实际对象,也就是栈和值的关系,比如 String a = new String();,这句代码就在堆内存中产生了一个 String 对象””,和栈内存中一个引用对象 a,也就是 a 指向了一个为空的字符串。当没有再次给引用对象 a 进行赋值时,操作 a 也即是操作这个空字符串。

后端和前端交互接口

Obiect 类有哪些方法

1、 getClass():获取类的 class 对象。
2、 hashCode:获取对象的 hashCode 值
3、 equals():比较对象是否相等,比较的是值和地址,子类可重写以自定义。
4、 clone():克隆方法。
5、 toString():如果没有重写,应用对象将打印的是地址值。
6、 notify():随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
7、 notifyall():解除所有那些在该对象上调用 wait 方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
8、 wait():导致线程进入等待状态,直到它被其他线程通过 notify()或者 notifyAll 唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。
9、 finalize():对象回收时调用

Java 数据类型

Java 中有 8 种基本数据类型,分别为:

6 种数字类型 :byte-1、short-2、int-4、long-8、float-4、double-8
1 种字符类型:char-2
1 种布尔型:boolean-1 位

这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean 。

包装类型不赋值就是 Null ,而基本类型有默认值且不是 Null。

基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;

从字节码中,我们发现装箱其实就是调用了 包装类的 valueOf()方法,拆箱其实就是调用了 xxxValue()方法

因此,

Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();

为什么存在这两种类型呢?

我们都知道在 Java 语言中,new 一个对象存储在堆里,我们通过栈中的引用来使用这些对象;但是对于经常用到的一系列类型如 int,如果我们用 new 将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,同 C++一样,Java 采用了相似的做法,对于这些类型不是用 new 关键字来创建,而是直接将变量的值存储在栈中,因此更加高效。

有了基本类型为什么还要有包装类型呢?

我们知道 Java 是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型 Collection 时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

另外,当需要往 ArrayList,HashMap 中放东西时,像 int,double 这种基本类型是放不进去的,因为容器都是装 object 的,这是就需要这些基本类型的包装器类了。

关键字总结

final 关键字

final 关键字,意思是最终的、不可修改的,用来修饰类、方法和变量,具有以下特点:

final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;

final 修饰的方法不能被重写

final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。

static 关键字

static 关键字主要有以下四种使用场景:

修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。

调用格式:类名.静态变量名 类名.静态方法名()

静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.

静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:

  1. 它的创建是不需要依赖外围类的创建。
  2. 不能使用任何外围类的非 static 成员变量和方法。

静态导包(用来导入类中的静态资源,1.5 之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。

this 关键字

this 关键字用于引用类的当前实例。 例如:
此关键字是可选的, 但是,使用此关键字可能会使代码更易读或易懂。

super 关键字

super 关键字用于从子类访问父类的变量和方法。 例如:

使用 this 和 super 要注意的问题:

在构造器中使用 super() 调用父类中的其他构造方法时该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。

this、super 不能用在 static 方法中。

被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。

而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this 和 super 是属于对象范畴的东西,而静态方法是属于类范畴的东西。

权限修饰符

image-20210805171804390

泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

==与 equals()?

==判断两个对象的地址是不是相等,对于基本数据类型:值。引用数据类型:内存地址。
equals()没有被覆盖重写,与==一样。

可以覆盖重写比较引用对象值的大小。
举例:String 中的 equals 方法是被重写过的。

hashcode()与 equal()?hashset 去重?

为什么要有 hashcode?

当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他已经加⼊的对象的 hashcode 值比较,如果没有相符的 hashcode, HashSet 会假设对象没有重复出现。

但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。

这样我们就⼤⼤减少了 equals 的次数,相应就⼤⼤提⾼了执⾏速度。
hash 优越的查询性能。

为什么重写 equals 必须重写 hashcode()?

对象相等,hashcode 一定相等,反之不一定(hash 碰撞)。
hashcode()默认对堆上面的对象产生独特值,hashcode 不同,对象一定不同。

举例:student s1=new student(name=”11”);
student s2=new student(name=”11”);

假如只重写 equals 而不重写 hashcode,那么默认的 hashcode 方法是根据对象的内存地址经哈希算法得来的,显然此时 s1!=s2。然而重写了 equals,且 s1.equals(s2)返回 true。

根据 hashcode 的规则,两个对象相等其哈希值一定相等,所以矛盾就产生了,因此重写 equals 一定要重写 hashcode。

重载与重写

  1. 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

    每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表

    最常用的地方就是构造器的重载。

  2. 重写(发生于运行期):子类继承父类的方法,输入一样,做出不同的操作就要覆盖重写父类方法。

方法名,参数列表必须一样。

抛出异常范围小于父类。
访问修饰符可以降低限制。

父类方法由 private,final,static 修饰时不能被重写。

抽象类与接口

抽象类和接口的区别

接口(interface)和抽象类(abstract class)是支持抽象类定义的两种机制。

接口是公开的,不能有私有的方法或变量,接口中的所有方法都没有方法体,通过关键字 interface 实现。

抽象类是可以有私有方法或私有变量的,通过把类或者类中的方法声明为 abstract 来表示一个类是抽象类,被声明为抽象的方法不能包含方法体。子类实现方法必须含有相同的或者更低的访问级别(public->protected->private)。抽象类的子类为父类中所有抽象方法的具体实现,否则也是抽象类。

接口可以被看作是抽象类的变体,接口中所有的方法都是抽象的,可以通过接口来间接的实现多重继承。接口中的成员变量都是 static final 类型,由于抽象类可以包含部分方法的实现,所以,在一些场合下抽象类比接口更有优势。

相同点

接口的实现类抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。

不同点

image-20210805171623086

抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为 abstract 方法,此时这个类也就成为 abstract 类了。

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:就可以把该抽象类改写为接口:interface。

那么接口的作用是什么呢?

1、Java 单继承的原因所以需要曲线救国 作为继承关系的一个补充。

2、把程序模块进行固化的契约,降低偶合。把若干功能拆分出来,按照契约来进行实现和依赖。(依赖倒置原则)

3、定义接口有利于代码的规范。(接口分离原则)

abstract class 表示的是 is a 关系interface 表示的是 like a 关系。

抽象类强调的是从属关系,接口强调的是功能。

抽象类作为很多子类的父类,它是一种模板式设计。

而接口是一种行为规范,它是一种辐射式设计。

对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;

而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

成员变量与局部变量?

根据定义变量位置的不同,可以将变量分为成员变量和局部变量
成员变量是在类范围内定义的变量
局部变量是在一个方法内定义的变量
1,成员变量可以分为:
实例属性 (不用 static 修饰)
随着实例属性的存在而存在
类属性 (static 修饰)
随着类的存在而存在
成员变量无需显式初始化,系统会自动对其进行默认初始化

2,局部变量可分为:
形参(形式参数)
在整个方法内有效
方法局部变量 (方法内定义)
从定义这个变量开始到方法结束这一段时间内有效
代码块局部变量 (代码块内定义)
从定义这个变量开始到代码块结束这一段时间内有效

局部变量除了形参外都必须显示初始化,也就是要指定一个初始值,否则不能访问。
java 允许局部变量和成员变量重名,局部变量会覆盖成员变量的值

面向对象

设计原则

  1. 单一职责原则(Single Responsibility Principle)

每一个类应该专注于做一件事情。

  1. 里氏替换原则(Liskov Substitution Principle)

超类存在的地方,子类是可以替换的。

  1. 依赖倒置原则(Dependence Inversion Principle)

实现尽量依赖抽象,不依赖具体实现。

  1. 接口隔离原则(Interface Segregation Principle)

应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。

  1. 迪米特法则(Law Of Demeter)

又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。

  1. 开闭原则(Open Close Principle)

面向扩展开放,面向修改关闭。

  1. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)

尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。

面向过程:以事件为中心,一步一步实现每个步骤。以步骤划分。

任务明确

效率高

扩展性差,复用性弱,逻辑需要深入思考。

面向对象:以对象为中心,以功能划分,每个对象都有自己的属性与行为。

程序模块化,结构化。

易于扩展,维护。

性能相对于面向过程低。

java 性能相对低的原因主要在于其是半编译语言。

封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性

就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。

继承

不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。

同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

关于继承如下 3 点请记住:

子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。

多态

多态,顾名思义,表示一个对象具有多种的状态。一种类型的变量可以引用多种实际类型的对象。实现方式: 继承(多个子类重写同一方法)
接口(实现接口并覆盖同一方法)

多态的特点:

对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
多态不能调用“只在子类存在但在父类不存在”的方法;
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

hash 冲突解决

开放地址法

​ 线性探测法:冲突之后,在紧跟着的地方安放数据。(大部分的数据都会聚集)。
​ 二次探测法:往后找 1,4,9,16,25,…位置有没有空位。
​ 再哈希法:多个散列函数。

链地址法:

String,StringBuffer,StringBuilder

String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。

在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串 private final byte[] value

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[] value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。

StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。

StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

异常体系

image-20210805134232290

自定义异常:继承 Exception 类。

异常的处理:

  1. 使用 try catch 捕获异常。可以有多个 catch,总会根据异常的类型优先找到第一个匹配的。因此,基类 exception 放在前面会导致后面的 catch 无法执行。

  2. 重新抛出异常。在 catch 中可以 throw 新的或者原来的异常,表示当前代码不能完全处理。可以通过 getCause()获取原始异常。

  3. try catch finally。finally 中语句始终会执行。适合释放资源,比如数据库连接,文件流释放等等。
    如果 try catch 中有 return,会在 finally 之后执行,但是 finally 不能改变其返回值。
    finally 中有 return,try catch 中的 return 会丢失,实际上会返回 finally 中的值。还会掩盖 try catch 中的异常。

  4. try with resourses,用于资源的释放,无需写 finally,会自动执行 close()。

  5. throws,声明一个方法可能抛出的异常。

异常体系可以:对异常集中处理,还可以向上传递,不需要每层都处理,也可以向上传递,也不会被自动忽略。处理异常情况的代码可以大大减少。

IO 流

image-20210805134331023

为何有了字节流,还要有字符流?

字符流是由 java 虚拟机将字节流转化而成。这个过程耗时,也容易出错。

所以提供了字符流,对于字符进行流操作。
而对于音频,图片等等推荐字节流。

编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 以GBK格式,读取文件
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, "GBK");
BufferedReader br = new BufferedReader(isr);
String str = null;
// 创建StringBuffer字符串缓存区
StringBuffer sb = new StringBuffer();
// 通过readLine()方法遍历读取文件
while ((str = br.readLine()) != null) {
// 使用readLine()方法无法进行换行,需要手动在原本输出的字符串后面加"\n"或"\r"
str += "\n";
sb.append(str);
}
String str2 = sb.toString();
// 以UTF-8格式写入文件,file.getAbsolutePath()即该文件的绝对路径,false代表不追加直接覆盖,true代表追加文件
FileOutputStream fos = new FileOutputStream(file.getAbsolutePath(), false);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write(str2);
osw.flush();
osw.close();
fos.close();
br.close();
isr.close();
fis.close();

java 8 新特性

Lambda 表达式

首先看看在老版本的 Java 中是如何排列字符串的:

1
2
3
4
5
6
7
8
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});

只需要给静态方法 Collections.sort 传入一个 List 对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给 sort 方法。

在 Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8 提供了更简洁的语法,lambda 表达式:

1
2
3
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});

看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:

1
Collections.sort(names, (String a, String b) -> b.compareTo(a));

对于函数体只有一行代码的,你可以去掉大括号{}以及 return 关键字,但是你还可以写得更短点:

1
Collections.sort(names, (a, b) -> b.compareTo(a));

Java 编译器可以自动推导出参数类型,所以你可以不用再写一次类型。

函数式接口

什么叫函数式编程?

就可以理解成用什么参数执行了一件什么事情,这就是函数式编程,它是匿名内部类进一步的简化,可以让代码更加的简洁。

但它有一个使用的前提,接口得是函数式接口。

有且仅有一个抽象方法需要被重写的接口。

这个怎么理解?很简单,函数式编程和匿名内部类相比,它省略了啥?

它省略了接口中的方法名,为什么可以省略?

因为就只有一个方法,那就算省略了方法名字,也知道是用的那个方法。

函数式接口: 只有一个方法的接口。

1
2
3
4
5
6
7
@FunctionalInterface

public interface Runnable {

public abstract void run();

}

// 简化编程模型,在新版本的框架底层大量应用!

image-20210805191320866

 

浏览器在接收到 html 文件后,会分几个步骤 html 文件转化成界面,这个过程就是渲染。

  1、解析 html

  2、构建 dom 树

  3、dom 树结合 css 文件,构建呈现树

  4、布局

  5、绘制

1、解析 html 和构建 dom 树是同步进行的,这个过程就是逐行解析代码,包括 html 标签和 js 动态生成的标签,最终生成 dom 树。

2、构建呈现树,就是把 css 文件和 style 标签的中的内容,结合 dom 树的模型,构建一个呈现树,写到内存,等待进一步生成界面。呈现树一定依赖 dom 树,呈现节点一定会有对应的 dom 节点,但是 dom 节点不一定会有对应的呈现节点,比如,被隐藏的一个 div。

3、布局,这一步就是结合呈现树,把 dom 节点的大小、位置计算出来。虽然呈现节点已经附着在都没节点上,会有对元素大小、位置的定义,但是浏览器还需要根据实际窗口大小进行计算,比如对 auto 的处理。

4、绘制,把 css 中有关颜色的设置,背景、字体颜色等呈现出来。

深拷贝浅拷贝

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

image-20210805200519957