Skip to content

java基础面试题

Updated: at 04:12 PM

1. 语言基础/String/static/final/函数传参

1.1. 基本数据类型和引用数据类型有什么区别?

引用数据类型在内存中不仅存储了对象的值(堆),还存储了指向堆中变量的引用。Object obj = new Object()在堆上创建了一个对象,并将该对象的引用赋值给obj

1.1.1. java基本数据类型

i. 字节流: byte,char

ii. 布尔值: boolean

iii. 整数: short, int, long

iv. 浮点数:float, double

1.1.2. java中char占几个字节?为什么占两个字节?

內码:某种语言在运行时,char和string在内存中的编码方式

外码:class文件存储字符的方式和java序列化编码

内码

采用字符集是unicode,是两字节

外码

采用的是utf-8

1.1.3. 请你说明int和Integer的区别?在[-128, 127]范围内Integer有什么特殊的吗?

如果两个⾮new⽣成的Integer变量进⾏⽐较的时候如果值在-128到127之间且值相等,则⽐较结果

为true,如果两个变量的值不在这个区间,⽐较的结果是false

为什么?

因为java在编译的时候对于非new创建的比如Integer i = 100会自动翻译为Integer i = valueOf(100),valueOf()方法对于-128 - 127会缓存,下次就不会再创建新的对象了。

1.2. String不可变设计?常量池?

  1. JVM中实现了常量池机制
  2. String不可变:private final char value[]用final修饰了,整个String类也是final的,所以不能被继承

原因:实现字符串池,节约heap空间,因为不同的字符串变量都指向池内的同一个字符串

1.2.1. intern()

intern会检查字符串常量池中是否有这个字符串(字面值比较),有则返回该字符串的引用,否则创建字符串对象并加到字符串常量池再返回引用

采用new创建的常量是会在堆上创建,但是不会进入常量池的,但是返回的引用是在栈上

字符串相加的时候,如果其中含有变量则不会进入字符串池中

String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");

str3 == str5 // false; 一个是堆上的引用, 一个直接是引用常量池
str5.intern() == str3 // true; // 都指向了常量池
str5.intern() == str4 // false; 字符串相加的时候,如果其中含有变量则不会进入字符串池中

1.2.2. String a = new String();生成了几个对象?String a = new String(“abc”);生成几个对象?

String a = new String(“abc”)

创建了一个或者两个对象,如果是常量池中已经有了,则只在堆上创建一个对象。如果是常量池中没有,则在堆上创建一个对象,也在常量池中创建一个对象。

1.2.3. 字符串常量池和运行时常量池

常量池表 用于存放编译期生成的各种字面量(#20 Hello World)和符号引用(#3 = string # 20)

当被加载到内存后,这些符号才有对应的内存地址信息,才被称为运行时常量池

JDK1.8之前运行时常量池和字符串常量池是在方法区,JDK1.8之后就放到堆中了。

1.2.4. StringBuffer内部是如何实现的?StringBuffer和StringBuilder的区别?

  1. StringBuilder是长度可变的,内部维护了默认长度为16的字符串,容量不足会自动扩容为2倍 + 2

  2. StringBuilder可以预先分配指定长度的内存块建立一个字符串缓冲区,这样比用String拼接更有效率,因为+操作符是每一次将字符添加到字符串中,字符串对象都需要寻找一个新的内存空间来容纳更大的字符串,这无疑是一个非常耗时的操作。添加多个字符也就意味着要一次又一次对字符串重新分配。

  3. StringBuffer是线程安全的,底层用synchronized来同步,但大多数场景下都是线程不安全的,因此可以用StringBuilder类(效率更高)

1.3. static用法

1.3.1. 静态方法可以被继承吗?可以被重写吗?

java中的静态属性和静态方法可以被继承,但是不能被重写,因为该方法可以属于类

1.4. final可以修饰什么?分别有什么特征?

  1. final变量:变量变常量,值不能被改变,与const相同。final必须要初始化。
  2. final方法:不能被重写
  3. final类:不能被继承
  4. final对象:指向对象的引用不可变,举个例子final Person person = new Person(xx),那么就不能再执行person = new Person(yy)了,类似于C++中的常量指针

1.5. Java只有值传递吗?

java中只有值传递

  1. 基础数据类型,传递对应的值,当函数内修改时,外面的值不会改变
  2. 包装数据类型,传递对象的引用的值。当函数内部修改时,值会变。

考点:Integer是不可变类,value是用final修饰的,重新赋值会创建一份copy,并使用copy替换原来的参数。

如下面的例子:

public class Calculate {

    public static void foo(Integer a) {
        a = 2;
    }
    public static void main(String[] args) {
        Integer a = 1;
        foo(a);
        System.out.println(a);
    }
}
//1

如上面的,foo()函数中的a = 2中的a是Integer包装类

public class Calculate {

    public static void foo(int a) {
        a = 2;
    }
    public static void main(String[] args) {
        int a = 1;
        foo(a);
        System.out.println(a);
    }
}
//1

包装类型值传递,在函数中修改对应的下标,原数据会变(包装类引用的引用)


import java.util.ArrayList;
import java.util.Arrays;

public class Calculate {

    public static void foo(ArrayList<Integer> arrayList) {
        arrayList.set(2, 0);
    }
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer> (Arrays.asList(1, 2, 3, 4, 5, 6));
        foo(list);
        System.out.println(list);
    }
}
//[1, 2, 0, 4, 5, 6]

2. Java OOP

2.1. 谈谈你对java面向对象的理解

基本理解:将问题分解成一个一个步骤,步骤抽象形成对象。通过对象的组合解决问题。

2.1.1. 面向对象三大基本特征?

三大基本特征:封装、继承、多态

封装

客观事物抽象成一个类,比如汽车类,发动机、轮胎抽象成属性,启动和停止等动作抽象成方法

继承
多态

子类有不同实现

2.1.2. 面向对象的五大基本原则?

单一职责原则

一个类只做好一件事,便于维护

开放封装原则

对扩展开放,对修改封装。比如形状接口,具体形状矩形和圆形实现该接口。修改矩阵类不会影响圆形类。

Liskov替换原则

子类必须能够替换父类,一个接收父类作为参数的函数也一定能接收一个子类作为参数的函数。

依赖倒置原则

依赖于接口而不是具体类

接口隔离原则

多个小接口替代大接口

2.2. Java如何体现面向对象中多态的特征/非继承关系的多态有什么/接口可以用来实现多态吗?

  1. 重载:同一个方法有不同的参数
  2. 重写:子类重写父类的参数
  3. 接口:interface,只定义不实现
  4. 抽象类与抽象方法 <1> 抽象类:在class前面添加了abstract修饰符,不用于实例化 <2>抽象方法没有定义,在方法前使用abstract修饰

2.2.1. 多态是如何实现的?多态是如何区分是调用的父类还是子类方法?

JVM运行时,类信息被存放在方法区,方法区有两个vtable表和interface表,分别对应invokevirtual和invokeinterface指令,这是java的动态分派。

每个类都有个vtable表,如果没有重写父类的,则指向相同的方法入口。如果重写了父类,则各自指向自己的方法入口,索引号一样。

2.2.2. 多态分为哪两种?如何表现?

多态分为 编译时多态运行时多态

其中编译时多态是静态的,主要是指方法的重载

运行时多态是动态绑定,1. 继承关系 2. 子类要重写父类的方法 3. 父类引用指向子类

2.2.3. 什么是内部类?什么是匿名内部类?

  1. 将一个类A定义在另一个类B里面,A类成为内部类,B类成为外部类
  2. 匿名内部类是一种特殊的内部类,它没有类名并且定义在另一个类或者方法中,用于创建一次性对象

2.3. 继承的各种特殊情况?

2.3.1. 接口里面可以定义方法体吗?

在JDK8之前不允许定义方法体

在JDK8之后允许定义default方法和static方法

2.3.2. 一个内部类可以引用它的类的成员吗?

如果内部类不是static就可以

原理:非静态内部类隐式持有对外部类的一个引用

2.4. 抽象类与抽象方法的各种情况

2.4.1. 抽象类与接口的区别?

  1. 抽象类被子类继承,接口要被子类实现
  2. 接口里面只能对方法进行声明,抽象类即可以对方法进行声明也可以
  3. 接口可以继承多个接口,类也可以继承多个接口,抽象类只能继承一个父类,类也只能继承一个抽象类

2.4.2. Override是必须的吗?

和C++一样,不是必须的,但是为了代码可读性,最好加override

2.4.3. 什么是抽象类?抽象类的特点?

在class前加abstract修饰

特点:不可以被实例化,有抽象方法也可以有方法实现

2.4.4. 抽象类可以有构造函数吗?

抽象类可以有构造函数,但不能被初始化。子类可以被初始化。

2.4.5. 抽象类里面必须有抽象方法吗?定义了抽象方法的一定是抽象类吗?

抽象类里面不一定有抽象方法

但是定义了抽象方法的一定是抽象类

2.4.6. 什么是虚方法,除了虚方法还有什么方法,虚方法有什么特殊的?

invokespecial和invokestatic在编译期解析,虚方法在运行时解析

2.5. Object下的所有方法

2.5.1. Object的方法

  1. getClass():返回对应的类
  2. clone():返回该对象的一个副本
  3. equals():比较
  4. hashCode():返回对象的哈希值
  5. wait():线程休眠
  6. notify():唤醒在该对象上等待的某个线程
  7. notifyAll():唤醒在该对象上等待的所有线程
  8. finalize():对象被垃圾收集器GC之前,finalize()方法会被调用。但是,调用时机不确定。

2.5.2. finalize()的方法

为什么不推荐重载finalize()方法?
  1. JVM不能保证finalize()方法能够执行完毕
  2. JVM在执行finalize()方法之后可能会重新执行执行GC(finalize()又申请了新的资源)

2.5.3. HashCode

HashCode的实现?

不同的HashCode方法实现不一样

  1. 普通的Object根据对象的内存地址返回一个hash值

  2. String是通过 左移和拿31去做乘

什么时候重写HashCode?

自定义类作为map的key的时候

2.5.4. equals与==

重写equal要满足哪些条件
  1. 自反性:x.equals(x) = true
  2. 对称性:x.equals(y) = true, y.equals(x) = true
  3. 传递性:x.equals(y) = true, y.equals(z) = true, x.equals(z) = true
  4. 一致性:多次调用x.equals(y)应返回相同的结果
==的比较规则
  1. Object:对比两个对象的内存引用地址
  2. 基本地址:比较数值
String、Integer的equals方法,Integer的==?

Integer在-127到128之间值相同可能是同一个(cache),所以Integer的==比较时如果在-127到128之间那就是true的。

Integer和String重写了equals(),所以比较的是值。

2.6. Java特性

2.6.1. 序列化与反序列化

java底层如何进行序列化的?

实现了Serializable接口的类,然后通过InputStream和OutputStream的writeObject,里面序列化fields,然后还会反射调用自己实现的序列化策略。

transient/static不能被序列化

transient有什么?

不会被序列化

2.6.2. 注解

有过哪些注解?

2.6.3. 泛型

java中怎么泛型的定义上界和下界?

使用extends和super

//上界
List<? extends Fruit> flistTop = new ArrayList<Apple>();
//下界
List<? super Apple> flistBottem = new ArrayList<Apple>();
Java中的泛型是什么?

Java中的泛型是擦除式泛型?即ArrayList和ArrayList其实是同一种类型,其在字节码中都被表示为原始的ArrayList类型,再进行强制类型转化。

Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));

Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));

2.6.4. 反射

反射原理?

每个对象都会指向一个方法区中的Class实例,这个Class有其对应的constant_pool,fields,methods等。

反射时invoke是委托给MethodAccessor来处理。每次反射新建Method对象,MethodAccessor有ReflectionFactory产生,而ReflectionFactory在15次以下调用使用Native方法,超过15次采用java方法。Native方法无法被java优化

Java反射机制的使用场景
反射为什么这么慢?

2.6.5. Timer

Timer定时器的用法

对比任务定时调度线程池,Timer只能单线程

首先创建TimerTask实现run方法,然后timer.schedule(…)即可

// 示例代码:
TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("当前时间: " + new Date() + "n" +
                "线程名称: " + Thread.currentThread().getName());
    }
};
System.out.println("当前时间: " + new Date() + "n" +
        "线程名称: " + Thread.currentThread().getName());
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);


//输出:
当前时间: Fri May 28 15:18:47 CST 2021n线程名称: main
当前时间: Fri May 28 15:18:48 CST 2021n线程名称: Timer
说一下timer定时器的时间原理

Timer底层使用一个单线程实现多个任务处理,意味着所有任务都是串行的

底层用了一个TaskQueue队列,内部使用堆排序,放在TimerTask[1]上的就是下一个要执行的。

Timer类有个TimerThread内部类,通过mainLoop包含一个while(true),从queue中获取任务(轮询)。

总结为:单线程优先队列+轮询

2.6.6. 异常处理

Exception之间的继承关系?

顶层是Throwable,往下Exception和Error继承自Throwable,Exception下面有RuntimeException(运行时异常)和编译时异常。

Exception是可以捕获的,Error是程序无法处理的错误(Class定义错误,虚拟机内存不够等)

RuntimeException主要包括:空指针异常,算术异常,类型转换异常

2.6.7. 动态代理

JDK动态代理的原理?

实现InvocationHandler接口,实现invoke(Object proxy, Method method, Object[] args);

proxy.newProxyInstance(classLoader, interfaces, invokeHandler);

CGLIB动态代理机制?

拦截器实现MethodInterceptor接口,实现intercept(Object o, Method method, Object[] args, MethodProxy method)

创建一个Enhancer,设置类加载器,设置方法拦截器为刚才实现MethodInterceptor。返回enhancer.create()

DEMO:

JDK动态代理
public class DebugInvocationHandler implements InvocationHandler {
    private final Object target;
    public DebugInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
	// ...其他操作,增强
        Object result = method.invoke(target, args);
    }
}
public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载器
                target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }
}
CGLIB动态代理
public class DebugMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // ...cglib动态代理增强
        Object object = methodProxy.invokeSuper(o, args);
        return object;
    }
}
public class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(clazz.getClassLoader()); // 设置类加载器
        enhancer.setSuperclass(clazz); // 设置被代理类
        enhancer.setCallback(new DebugMethodInterceptor());  // 设置方法拦截器
        return enhancer.create();
    }
}
JDK动态代理和CGLIB动态代理的区别?
  1. JDK动态代理采用的反射机制,只能代理实现了接口的类或者直接代理接口,通过实现目标类接口的方法来动态创建,CGLIB使用的是ASM字节码框架在运行时动态创建对象,可以代理未实现任何接口的类。
  2. CGLIB动态代理是通过生成一个被代理的类的子类来拦截方法调用的,因此不能代理声明为final类型的类和方法
  3. JDK动态代理的效率更高。

2.6.8. JDK新特性

JDK8
interface
  1. default方法

  2. static方法,只能通过接口调用

  3. 接口的方法是public abstract修饰,变量是public static final修饰

functional interface函数式接口

有且只有一个抽象方法

Lamda表达式

参数是functional interface,可以用Lamda表达式代替

格式:

(parameters) -> expression 或
(parameters) ->{ statements; }

方法引用:

Java 8 允许使用 :: 关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。

Stream
  1. 链式编程
  2. 方法参数都是function interface
  3. 一个stream只能操作一次,操作完就关了
  4. 速度快

2.6.9. 深拷贝和浅拷贝

实现cloneable,重写clone()

DEMO:

@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}