Java后端高频知识点学习笔记1---Java基础
Java后端高频知识点学习笔记1---Java基础
参考地址:牛_客_网
https://www.nowcoder.com/discuss/819297
1、重载(Overload)和重写(Override)的区别
重载:同一类中多个同名方法根据不同的传参来执行不同的处理逻辑;方法名必须相同,参数列表不同(个数、类型、类型不同+顺序不同),返回值类型可以相同也可以不同(因为返回值类型不是方法签名的一部分)
原理:方法名称相同时,编译器会根据调用方法的参数个数,参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。
重写:子类对父类的方法的实现过程进行重新编写。方法名,参数列表和返回值类型都不能改变。抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
什么是方法签名?
答:方法签名是区分不同方法的标识符;方法是由方法名、形参列表、返回值以及方法体构成;方法签名是由方法名与形参列表构成,也就是说,方法名和形参列表可以唯一地确定一个方法,与方法的返回值没有关系
构造器是否可以被重写,是否可以被重载?
答:构造器可以被重载(Overload),不能被重写(Override)
静态方法能否被重写,能否被重载?
答:静态方法不能被重写,可以被重载
静态方法可以被继承;静态方法:在类加载时,被加载到内存中的方法,在整个运行过程中保持不变,因而不能重写;在Java中,如果父类中含有一个静态方法,且在子类中也含有一个返回类型、方法名、参数列表均与之相同的静态方法,那么该子类实际上只是将父类中的该同名方法进行了隐藏,而非重写,可以通过类名.方法名调用被隐藏的方法;换句话说,父类和子类中含有的其实是两个没有关系的方法,它们的行为也并不具有多态性
2、Java面向对象的三大特性
封装:把一个对象的属性私有化,不允许外部对象直接访问这些私有属性,同时向外界提供一些访问私有属性的方法
继承:子类继承父类的非私有属性和方法;子类可以对父类的方法进行重写;也可以进行扩展,拥有自己的属性和方法;一个子类只能拥有一个父类,但是可以通过实现多个接口来达到多重继承的目的
多态:同一个操作作用在不同对象时,可以产生不同的执行结果;在Java语言中,多态主要有两种表现形式;
___编译时多态-->重载
___运行时多态-->重写
- 重载:同一个类中有多个同名方法,根据不同的传参可以执行不同的处理逻辑;在编译时就可以确定到底调用哪个方法,它是一种编译时多态
- 重写:子类对父类的方法的实现过程进行重新编写,方法名,参数列表和返回值类型都不能改变,因此同样的方法在父类与子类中有着不同的表现形式;
Java语言中,父类的引用变量不仅可以指向父类的实例对象,也可以指向子类的实例对象,程序调用的方法在运行时才动态绑定
实际调用的是引用变量所指向的具体实例对象的方法,而不是引用变量的类型中的定义的方法;这就会出现相同类型的变量调用同一个方法时呈现出多种不同的行为特征,这就是多态;在运行时才能确定调用哪个方法,被称为运行时多态
使用多态的好处?
1、应用程序不必为每一个子类编写功能调用,只需要对抽象父类进行处理即可,提高程序的可复用性
2、子类的功能可以被父类的引用变量所调用,提高可扩充性和可维护性
3、Java面向对象的5大设计原则
原则 | 描述 |
---|---|
单一职责 | 一个类只负责一个功能的实现 |
里氏替换 | 只要父类出现的地方,都可以用子类替换 |
依赖倒置 | 高层模块不应该依赖低层模块,二者都应该依赖其抽象;就是面向接口编程 |
接口隔离 | 接口的功能尽可能单一;接口更可能细化,不要建立臃肿庞大的接口 |
开闭 | 尽量通过扩展来面对需求的更改或者系统的变化,尽量不要对原有内容修改 |
4、String、StringBuilder和StringBuffer的区别是什么?
(1)String是不可变的,StringBuilder和StringBuffer是可变的
- String不可变,String类中 使用final关键字修饰的字符数组 来保存字符串;private final char value[];从Java9开始,String类改用final修饰的字节数组存储字符串; private final byte[] value;
- StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中使用没有final关键字修饰的字符数组保存字符串;char[] value,所以这两种对象都是可变的
(2)String和StringBuffer是线程安全的,StringBuilder不是线程安全的
- String中的对象的不可变,StringBuffer对方法加了synchronized同步锁,所以是线程安全的
StringBuilder没有对方法加同步锁,所以不是线程安全的
(3)执行效率:StringBuilder最高,StringBuffer次之,String最低
- 每次对String类型进行改变的时候,都会生成一个新的String对象,然后将引用变量指向新的String对象;
- StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用,StringBuilder同理;
- 相同情况下使用StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但要冒多线程不安全的风险
总结:
- 当操作少量数据时,优先使用 String 类 -->> (线程安全,效率最低)
- 当在单线程下操作大量数据,优先使用 StringBuilder 类 -->> (线程不安全,效率最高)
- 当在多线程下操作大量数据,优先使用 StringBuffer 类 -->> (线程安全,效率中等)
5、String为什么要设置成不可变的?
(1)实现字符串常量池
- 字符串常量池(String pool)是Java堆内存中一个特殊的存储区域,当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象,严格来说,这种常量池的思想,是一种优化手段
(2)允许String对象缓存HashCode
- Java中String对象的哈希码被频繁地使用,比如在HashMap等容器中;字符串的不变性保证了hash码的唯一性;因此可以放心地进行缓存,这也是一种性能优化手段,意味着不必每次都去计算新的哈希码
(3)安全性
- String被许多的Java类(库)用来当做参数,例如:网络连接地址URL,文件路径path,还有反射机制所需要的String参数等,假若String不是固定不变的,将会引起各种安全隐患;
数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入;因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
在并发场景下,多个线程同时读写资源时,由于 String 是不可变的,不会引发线程的问题而保证了线程安全
总体来说,String不可变的原因包括设计、效率优化 与 安全性这三大方面
6、自动装箱与拆箱
装箱:将 基本数据类型 转化为 包装器类型;装箱调用 Integer.valueOf() 方法
拆箱:将 包装器类型 转化为 基本数据类型;拆箱调用 Integer.intValue() 方法
如下代码:
Integer i = 100; // 实际上是Integer.valueOf(100)
int n = i; // 实际上是 i.intValue()
如上代码所示,在装箱的时候自动调用的是Integer的valueOf(int)方法;而在拆箱的时候自动调用的是Integer(对象)的intValue()方法
详解:在调用Integer.valueOf()方法中,如果数值在[-128,127]之间,将直接从IntegerCache中获取
因为IntegerCache中缓存了[-128,127]之间的值,通过Integer.valueOf()方法创建数值在[-128,127]之间的Integer对象时,便返回指向IntegerCache中已经存在的对象的引用;否则创建一个新的Integer对象
7、抽象类和接口的异同?
- 不同点
序号 | 不同点 |
---|---|
1、方法的定义与实现 | 在JDK1.8之前接口只有方法的定义,不能有方法的实现;JDK1.8中接口可以有默认方法(default修饰)和静态方法(static修饰)的实现;JDK1.9开始接口中可以定义和实现私有方法(普通私有方法和静态私有方法);而抽象类可以有方法的定义与实现 |
2、静态常量与普通成员变量 | 接口里只能定义静态常量(static final),不能定义普通成员变量;抽象类中既可以定义静态常量,也能定义普通成员变量 |
3、静态代码块 | 接口中不能包含静态代码块,抽象类中可以包含静态代码块 |
4、继承 | 一个类只能继承一个抽象类,但是一个类可以实现多个接口 |
5、*** | 接口强调的是特定功能的实现,抽象类强调的是所属关系 |
6、main()方法 | 接口不能有 main 方法;抽象类可以有 main 方法,并且可以运行它 |
7、默认修饰符 | 接口中定义的成员变量,只能是静态常量,默认修饰符 public static final,而且必须给其赋初值;接口中定义的成员方法,抽象方法,默认修饰符为public abstract;抽象类中成员变量默认default(默认,什么也不写,同一包中可见),可在子类中被重新定义,也可被重新赋值;抽象类中抽象方法被abstract修饰,不能被private、static、synchronzed和native等修饰,必须以分号结尾,不带花括号 |
8、构造器 | 接口中不包含构造器;抽象类里可以包含构造器,抽象类中的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作 |
9、使用场景 | 接口被用于常用的功能,便于日后维护和添加删除;抽象类更倾向于充当公共类的角色,不适用于日后重新对内部的代码修改;功能需要累积时用抽象类,不需要累积时用接口 |
- 相同点
序号 | 相同点 |
---|---|
1 | 接口和抽象类都不能被实例化;接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化 |
2 | 接口和抽象类都可以包含抽象方法 |
8、Java中" == "与equals()方法的区别?
" == ":对于八大基本数据类型来说,直接比较值;如果是引用数据类型,则是比较内存地址是否相同;(因为Java只有值传递,所以对于" == "来说,不管是比较基本数据类型,还是引用数据类型的变量,本质都是比较值,只是引用类型变量存的值是对象的地址)
equals():equals()是Object类提供的方法之一;每一个Java类都继承自Object类,所以每一个对象都具有equals方法;Object中的equals方法是直接使用" == "运算符比较两个对象,所以在没有重写equals方法的情况下,equals与" == "运算符一样,比较的是地址;可以通过重写equals方法来比较两个对象的内容是否相等
9、为什么重写equals()方法时必须重写hashCode()方法?
equals()和hashCode()方法要遵循如下的原则:
1、如果两个对象equals()方法相等,它们的hashCode返回值一定要相同
2、如果两个对象的hashCode()返回值相同,但它们的equals()方法不一定相等
3、如果两个对象的hashcode()返回值不相等,则两个对象的equals()一定不相等
如果只重写equals()方法而不重写hashCode()方法,将会造成equals()方法判断出的结果是true,但hashCode()返回值不同的情况,也可能会出现hashCode()方法返回值不相等,但equals()方法相同的情况,因此equals()方法与hashCode()方法都要进行修改,使两者遵循上述原则;即:equals方法被覆盖,则hashCode方法也必须被覆盖,始终遵循上述原则
问题:为什么需要hashCode()方法?
答:使用hashCode()方法提前校验,避免每一次比较都调用equals方法,提高效率
面试官:你有没有重写过equals()和hashcode()?
答:在使用HashMap的“key”的部分存放自定义的对象时,重写过hashCode和equals方法,从而保证key是唯一的
10、Java中的八大基本数据类型
Java有8中基本数据类型,其中包括
6种数字类型:byte(1)、short(2)、int(4)、long(8)、float(4)、double(8)
1种字符类型:char(2)
1种布尔型:boolean
对于boolean型,官方文档未明确定义,它依赖于JVM厂商的具体实现
11、final关键字
final用来修饰类、方法和变量
1、final修饰的类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法(但是final修饰的类中成员变量是可变的,如果想要final类的成员变量不可变,必须给成员变量增加final修饰)
2、final修饰的方法不能被重写
3、final修饰的变量是常量;如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象,即引用类型变量始终指向同一地址
12、static关键字
static关键字主要有以下4种用法:
1、修饰类成员(成员变量 和 成员方法)
被static修饰的成员属于这个类,被类的所有对象共享,可通过 类名.成员名 或 对象.成员名 的方式调用
-
static修饰的变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化;而非静态变量是实例对象所独有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响;static成员变量的初始化顺序按照定义的顺序进行初始化
-
static修饰的方法也称作静态方法,静态方法不依赖于任何对象就可以进行访问,静态方法只能访问静态成员,不能访问类的非静态成员,因为非静态成员必须依赖具体的对象才能够被调用(成员:成员方法与成员变量)
2、静态代码块
静态代码块定义在类中方法外,类中可以有多个static块
静态代码块在非静态代码块之前;按照static块的顺序来执行 (静态代码块-->非静态代码块-->构造方法)
一个类不管创建多少对象,静态代码块只执行一次
初始化的加载次序:
父类静态代码块-->子类静态代码块-->父类非静态代码块-->父类构造函数-->子类非静态代码块-->子类构造函数
3、静态内部类
static修饰类的话只能修饰内部类;静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:(1)它的创建是不需要依赖外围类的创建;(2)它不能使用任何外围类的非静态成员(成员:成员方法与成员变量)
4、静态导包
导入类中的静态资源,1.5之后的新特性;可以指定导入某个类中的指定静态资源,并且不需要通过类名调用类中的静态成员,可以直接使用类中静态成员变量和成员方法;
优点:静态导入可减少代码输入量
注意:当两个不同的类中有命名相同的方法都使用静态导入时,仍然需要通过 类名.方法名 的方式进行调用,否则会报错!
- 导入语法:
import static 包名.类名.静态成员变量;
import static 包名.类名.静态成员方法;
import static 包名.类名.*;
13、深拷贝和浅拷贝
浅拷贝:对基本数据类型进行值传递;对引用数据类型只是进行引用的传递,并没有在内存中的创建一个新的对象
深拷贝:对基本数据类型进行值传递;对引用数据类型,先创建一个新的对象,再复制其内容,最后传递新对象的引用
14、Java异常体系
Java中,所有的异常都有⼀个共同的祖先java.lang包中的Throwable类:Throwable类有两个重要的⼦类:Exception(异常) 和 Error(错误),⼆者都是Java异常体系的重要子类,各自都包含大量子类
Error(错误):程序⽆法处理的错误,表示运⾏应⽤程序中较严重问题;⼤多数错误与代码编写者执⾏的操作⽆关,⽽表示代码运⾏时JVM出现的问题;例如,Java 虚拟机运⾏错误(Virtual MachineError),当 JVM 不再有继续执⾏操作所需的内存资源时,将出现内存溢出(OutOfMemoryError);这些异常发⽣时,JVM⼀般会选择终⽌线程
Exception(异常):程序本身可以处理的异常;Exception 类有⼀个重要的⼦类RuntimeException;RuntimeException异常由JVM抛出;还有NullPointerException(要访问的变量没有引⽤任何对象时,抛出该异常);ArithmeticException(算术运算异常,⼀个整数除以0时,抛出该异常)以及 ArrayIndexOutOfBoundsException(数组下标越界异常)
Exception(异常)又分为两类:运行时异常、编译时异常
- 1、运行时异常(不受检异常):RuntimeException类及其子类表示JVM在运行期间可能出现的错误;比如说试图使用空值对象的引用(NulIPointerException)、数组下标越界(ArraylndexOutBoundException);此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理
2、编译时异常(受检异常):如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,否则编译不通过;在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类
15、反射(待补充完善)
1、什么是Java的反射
反射 是 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制
2、反射的作用
运行时,判断对象所属的类;判断一个对象的方法和属性;调用对象的方法和属性
16、Java泛型
Java的泛型即"参数化类型",允许程序在创建集合时指定该集合元素的类型,表示该集合只能保存该类型的对象
- 为什么要使用泛型?
如果不使用泛型,当把一个对象存入集合后,集合就会忘记这个对象的数据类型,再次取出该对象时,该对象的编译类型就变成了Object类型,还需要进行强制转换;
当使用泛型后,集合中只能存入指定类型的对象,否则将报错,并且将对象从集合取出后无需对元素进行强制转换,就是原本指定的类型; - **作用:
-
- 1、防止了 "在集合中存储对象需要在使用前进行类型转换" 情况的发生,提供了编译时的类型安全**
-
- 2、确保只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException(类型转换异常)
扩展知识:
父子对象之间的转换分为了向上转型 和 向下转型;它们解释如下:
向上转型:通俗地讲,就是将子类对象向上转为父类对象,或者说是父类引用指向子类对象,此处父类对象可以是接口
格式:父类 变量名 = new 子类();
向上转型的好处?
向上转型后,父类引用可以调用子类重写(Override)过的父类方法,当需要新添功能时,可以新增一个(子)类即可,避免修改原来的代码
向上转型后的对象不是新创建的父类对象,而是子类对象的"简化"状态,它不关心子类新增的功能,只关心子类继承和重写的功能;
当一个类有很多子类时,并且这些子类都重写了父类中的某个方法,使用上转型对象调用这个方法时就可能具有多种形态,因为不同的子类在重写父类的方法时可能产生不同的行为;
也就是说,不同对象的上转型对象调用同一方法可能产生不同的行为
向下转型:与向上转型相反,即是把(已向上转型的)父类对象** 转为 子类对象;一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制转换的格式**
格式:子类 变量名 = (子类类型)父类变量;
- 为什么要向下转型?
向下转型一般是为了重新获得因为向上转型而丢失的子类特性;因此,通常在向下转型前已经有向上转型,而向下转型通常也会结合instanceof一起使用;借由向下转型,可以在灵活应用多态的基础上,同时兼顾子类的独有特征(instanceof:用来测试一个对象是否为一个类的实例)
用法是:
boolean result = obj instanceof Class; //测试 obj 是否为 Class 的实例
17、什么是泛型擦除
在代码中定义List<Object>
和List<String>
等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的
在如下例子中,定义了两个ArrayList数组;list1只能存储字符串,list2只能存储整数;通过list1对象和list2对象的getClass()方法获取他们的类信息,最后发现结果为true
说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型
//Java代码
import java.util.*;
public class Test {
public static void main(String[] args) {
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
System.out.println("l1.getClass():"+l1.getClass().getName());
System.out.println("l1.getClass():"+l2.getClass().getName());
}
}
运行结果:
true
l1.getClass():java.util.ArrayList
l1.getClass():java.util.ArrayList
18、Java Object类中的方法
方法 | 含义 |
---|---|
getClass() | 返回一个对象的运行时类 |
int hashCode() | 返回该对象的哈希码值 |
boolean equals(Object obj) | 判断其他对象是否与此对象 “相等” |
String toString() | 返回该对象的 “字符串表示” |
void notify() | 唤醒在此对象监视器上等待的单个线程 |
void notifyAll() | 唤醒在此对象监视器上等待的所有线程 |
void wait() | 导致当前线程等待并放弃锁的持有,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法;当前线程必须是此对象的监视器所有者,否则还是会发生 IllegalMonitorStateException 异常 |
protected Object clone() | 创建并返回此对象的一个副本(浅拷贝,拷贝引用地址,不在内存之创建新对象并指向它) |
protected void finalize() | 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法 |
19、Java String类中的方法
20、Java创建对象的5种方式?
① 使用new关键字
② 使用Class类的newInstance方法
③ 使用Constructor类的newInstance方法
④ 使用clone方法
⑤ 使用反序列化
21、Java访问修饰符的范围
22、Hash冲突的解决方式?
1、开放定址法
当发生冲突时,使用某种探查技术在散列表中形成一个探查序列,沿此序列逐个单元地查找,直到找到一个开放的地址(地址单元为空)或探查序列查找完为止;若探查到开放的地址,则可将待插入的新结点存人该地址单元;若探查序列查找完都没有找到开放的地址,则失败
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、随机探查法
① 线性探查法
② 二次探查法
③ 随机探查法
2、拉链法
将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存到哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行
拉链法(链地址法)适用于经常进行插入和删除的情况
3、再Hash法
当计算出的hash值产生冲突时,再计算另一个Hash函数的哈希值,直到冲突不再发生为止
4、建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
23、Java中的流分为几种?
按照流的流向划分,分为 输⼊流 和 输出流
按照操作单元划分,分为 字节流 和 字符流
按照流的角色划分,分为 节点流 和 处理流
Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,⽽且彼此之间存在⾮常紧密的联系;Java IO流的 40 多个类都是从如下4个抽象类基类中派⽣出来的;
InputStream、InputReader: 所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流
OutputStream、OutputWriter: 所有输出流的基类,前者是字节输出流,后者是字符输出流