Java基础知识面试题
访问修饰符
访问修饰符 public,private,protected,以及不写(默认)时的区别
● 定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
● 分类
○ private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
○ default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
○ protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
○ public : 对所有类可见。使用对象:类、接口、变量、方法
访问修饰符图
运算符
&和&&的区别
● &运算符有两种用法:(1)按位与;(2)逻辑与。
● &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
final 有什么用?
用于修饰类、属性和方法;
● 被final修饰的类不可以被继承
● 被final修饰的方法不可以被重写
● 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
static存在的主要意义
● static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
● static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
● 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static的独特之处
● 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?
● 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
● 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
● 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
static应用场景
● 因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
● 因此比较常见的static应用场景有:
1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包
static注意事项
● 1、静态只能访问静态。
● 2、非静态既可以访问非静态的,也可以访问静态的。
类与接口
抽象类和接口的对比
● 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
● 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
相同点
● 接口和抽象类都不能实例化
● 都位于继承的顶端,用于被其他实现或继承
● 都包含抽象方法,其子类都必须覆写这些抽象方法
不同点
备注:
Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。
现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
普通类和抽象类有哪些区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
创建一个对象用什么关键字?对象实例与对象引用有何不同?
new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)
内部类
什么是内部类?
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。
内部类的分类有哪些
内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。
静态内部类
定义在类内部的静态类,就是静态内部类。
静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类(),如下:
成员内部类
定义在类内部,成员位置上的非静态类,就是成员内部类。
成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类(),如下:
局部内部类
定义在方法中的内部类,就是局部内部类。
定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类(),如下:
匿名内部类
匿名内部类就是没有名字的内部类,日常开发中使用的比较多。
除了没有名字,匿名内部类还有以下特点:
匿名内部类必须继承一个抽象类或者实现一个接口。
匿名内部类不能定义任何静态成员和静态方法。
当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
匿名内部类创建方式:
内部类的优点
我们为什么要使用内部类呢?因为它有以下优点:
一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
内部类不为同一包的其他类所见,具有很好的封装性;
内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
匿名内部类可以很方便的定义回调。
内部类有哪些应用场景
一些多算法场合
解决一些非面向对象的语句块。
适当使用内部类,使得代码更加灵活和富有扩展性。
当某个类除了它的外部类,不再被其他的类使用时。
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码:
以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
内部类相关,看程序说出运行结果
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}复制代码
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();复制代码
public class Outer {
private static int radius = 1;
private int count =2;
class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}复制代码
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();复制代码
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}复制代码
public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}复制代码
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}复制代码
new 类/接口{
//匿名内部类实现部分
}复制代码
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}复制代码
public class Outer {
private int age = 12;
class Inner {
private int age = 13;
public void print() {
int age = 14;
System.out.println("局部变量:" + age);
System.out.println("内部类变量:" + this.age);
System.out.println("外部类变量:" + Outer.this.age);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
}
}复制代码
重写与重载
构造器(constructor)是否可被重写(override)
● 构造器不能被继承,因此不能被重写,但可以被重载。
重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
● 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
● 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
● 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
对象相等判断
== 和 equals 的区别是什么
● == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
● equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
○ 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
○ 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
○ 举个例子:
● 说明:
○ String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
○ 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
hashCode 与 equals (重要)
● HashSet如何检查重复
● 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
● hashCode和equals方法的关系
● 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
hashCode()介绍
● hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
● 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
● 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
hashCode()与equals()的相关规定
● 如果两个对象相等,则hashcode一定也是相同的
● 两个对象相等,对两个对象分别调用equals方法都返回true
● 两个对象有相同的hashcode值,它们也不一定是相等的
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
对象的相等与指向他们的引用相等,两者有什么不同?
● 对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}复制代码
ArrayList和LinkedList区别
arrayList:
底层底层基于数组实现的,支持对元素进行随机访问,适合随机查找和遍历,实际上不适合插入和删除,默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中,当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制,移动代价比较高。
LinkedList:
底层基于双向链表实现的,适合数据的动态插入和删除,内部提供了list接口
中没有定义的方法,用于操作表头和表尾的元素,可以当作堆栈,队列和双向队列使用,(比如jdk官方推荐使用基于linkedList适用于增加删除多的场景)
区别
都是线程不安全的,arrayList适用于查找的场景,LinkedList适用增加,删除多的场景
实现线程安全
可以使用原生的Vector,或Collections.synchronizedList(List list)返回一个线程安全的ArrayList集合,建议使用juc并发包下的CopyOnWriteArrayList
Vector底层通过synchronize修饰保证线程安全,效率差
CopyOnWriteArrayList:写时加锁使用了一种叫写时复制的方法,读操作是不用加锁的
HashMap
hashmap在底层数据结构上采用数组+链表+红黑树,通过散列映射来存储键值对数据
扩容情况:
默认的负载因子是0.75,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作,
put操作步骤:
1判断数组是否为空
2不为空则计算key的hash值,通过(n-1)&hash计算应当存放在数组中的下表index
3查看table[index]是否存在数据,没有数据就构造一个node节点存放在table[index]中
4存在数据,说明发生了hash冲突,继续判断key是否相等,相等用新的value替换原数据
5若不相等,判断当前节点类型是不是是不是树形节点,如果是树形节点,创造树形节点插入红黑树中
6若不是红黑树,创建普通node加入链表中,判断链表长度是否大于8,大于则将链表转换为红黑树,插入完成之后判断当前节点数,是否大于阈值,若大于则扩容为原数组的二倍
hash函数:
通过hash函数先拿到key的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行异或操作,该函数称为扰动函数,做到尽可能降低hash碰撞,通过尾插法进行插入
jdk1.7和1.8的区别
jdk1.7和jdk1.8区别:
1.7底层是数组和链表,结合在一起使用也就是链表散列,如果相同的话直接覆盖,不相同就通过拉链法解决冲突,扩容翻转时顺序不一致,使用头插法会产生死循环,导致cpu100%
1.8采用数组+链表+红黑树 当链表长度大于阈值8,数组长度大于64时,链表将转化为红黑树,以减少搜索时间。
ConcurrentHashMap
ConcurrentHashMap和hashtable来实现线程安全,hashtable是原始API类,通过synchronize同步修饰,效率低下,ConcurrentHashMap通过分段锁实现,效率较比hashtable要好
AQS抽象的队列式同步器
是一个用来构建锁和同步器的框架