• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

Java后端开发面试问题总结(上)

技术杂谈 勤劳的小蚂蚁 3个月前 (01-06) 96次浏览 已收录 0个评论 扫描二维码


  • 1、Hashmap是怎么实现的,底层原理?
  • 2、Java中的错误和异常?
  • 3、Java的集合类框架介绍一下?
  • 4、Java反射是什么?为什么要用反射,有什么好处,哪些地方用到了反射?
  • 5、说说你对面向对象、封装、继承、多态的理解?
  • 6、实现不可变对象的策略?比如JDK中的String类。
  • 7、Java序列话中如果有些字段不想进行序列化,怎么办?
  • 8、==和equals的区别?
  • 9、接口和抽象类的区别?
  • 10、给你一个Person对象p,如何将该对象变成JSON表示?
  • 11、JDBC中sql查询的完整过程?操作事务呢?
  • 12、实现单例,有哪些要注意的地方?


秋招面试题总结

Java基础

1、Hashmap是怎么实现的,底层原理?

JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n-1)&hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
  1.      staticfinalint hash(Object key){
  2.        int h;
  3.        // key.hashCode():返回散列值也就是hashcode
  4.        // ^ :按位异或
  5.        // >>>:无符号右移,忽略符号位,空位都以0补齐
  6.        return(key ==null)?0:(h = key.hashCode())^(h >>>16);
  7.    }
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
  1. staticint hash(int h){
  2.    // This function ensures that hashCodes that differ only by
  3.    // constant multiples at each bit position have a bounded
  4.    // number of collisions (approximately 8 at default load factor).

  5.    h ^=(h >>>20)^(h >>>12);
  6.    return h ^(h >>>7)^(h >>>4);
  7. }
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8之后

相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
推荐阅读:
  • 《Java 8系列之重新认识HashMap》 :https://zhuanlan.zhihu.com/p/21673805

2、Java中的错误和异常?

Java中的所有异常都是Throwable的子类对象,Error类和Exception类是Throwable类的两个直接子类。
Error:包括一些严重的、程序不能处理的系统错误类。这些错误一般不是程序造成的,比如StackOverflowError和OutOfMemoryError。
Exception:异常分为运行时异常和检查型异常。
  • 检查型异常要求必须对异常进行处理,要么往上抛,要么try-catch捕获,不然不能通过编译。这类异常比较常见的是IOException。
  • 运行时异常,可处理可不处理,在编译时可以通过,异常在运行时才暴露。比如数组下标越界,除0异常等。

3、Java的集合类框架介绍一下?

首先接口Collection和Map是平级的,Map没有实现Collection。
Map的实现类常见有HashMap、TreeMap、LinkedHashMap和HashTable等。其中HashMap使用散列法实现,低层是数组,采用链地址法解决哈希冲突,每个数组的下标都是一条链表,当长度超过8时,转换成红黑树。TreeMap使用红黑树实现,可以按照键进行排序。LinkedHashMap的实现综合了HashMap和双向链表,可保证以插入时的顺序(或访问顺序,LRU的实现)进行迭代。HashTable和HashMap比,前者是线程安全的,后者不是线程安全的。HashTable的键或者值不允许null,HashMap允许。
Collection的实现类常见的有List、Set和Queue。List的实现类有ArrayList和LinkedList以及Vector等,ArrayList就是一个可扩容的对象数组,LinkedList是一个双向链表。Vector是线程安全的(ArrayList不是线程安全的)。Set的里的元素不可重复,实现类常见的有HashSet、TreeSet、LinkedHashSet等,HashSet的实现基于HashMap,实际上就是HashMap中的Key,同样TreeSet低层由TreeMap实现,LinkedHashSet低层由LinkedHashMap实现。Queue的实现类有LinkedList,可以用作栈、队列和双向队列,另外还有PriorityQueue是基于堆的优先队列。

4、Java反射是什么?为什么要用反射,有什么好处,哪些地方用到了反射?

反射:允许任意一个类在运行时获取自身的类信息,并且可以操作这个类的方法和属性。这种动态获取类信息和动态调用对象方法的功能称为Java的反射机制。
反射的核心是JVM在运行时才动态加载类或调用方法/访问属性。它不需要事先(写代码的时候或编译期)知道运行对象是谁,如 Class.ForName()根本就没有指定某个特定的类,完全由你传入的类全限定名决定,而通过new的方式你是知道运行时对象是哪个类的。 反射避免了将程序“写死”。
反射可以降低程序耦合性,提高程序的灵活性。new是造成紧耦合的一大原因。比如下面的工厂方法中,根据水果类型决定返回哪一个类。
  1. publicclassFruitFactory {
  2.    publicFruit getFruit(String type) {
  3.        Fruit fruit = null;
  4.        if ("Apple".equals(type)) {
  5.            fruit = newApple();
  6.        } elseif ("Banana".equals(type)) {
  7.            fruit = newBanana();
  8.        } elseif ("Orange".equals(type)) {
  9.            fruit = newOrange();
  10.        }
  11.        return fruit;
  12.    }
  13. }

  14. classFruit {}
  15. classBananaextendsFruit {}
  16. classOrangeextendsFruit {}
  17. classAppleextendsFruit {}
但是我们事先并不知道之后会有哪些类,比如新增了Mango,就需要在if-else中新增;如果以后不需要Banana了就需要从if-else中删除。这就是说只要子类变动了,我们必须在工厂类进行修改,然后再编译。如果用反射呢?
  1. publicclassFruitFactory {
  2.    publicFruit getFruit(String type) {
  3.        Fruit fruit = null;
  4.        try {
  5.            fruit = (Fruit) Class.forName(type).newInstance();
  6.        } catch (Exception e) {
  7.            e.printStackTrace();
  8.        }
  9.        return fruit;
  10.    }
  11. }

  12. classFruit {}
  13. classBananaextendsFruit {}
  14. classOrangeextendsFruit {}
  15. classAppleextendsFruit {}
如果再将子类的全限定名存放在配置文件中。
  1. class-type=com.fruit.Apple
那么不管新增多少子类,根据不同的场景只需修改文件就好了,上面的代码无需修改代码、重新编译,就能正确运行。
哪些地方用到了反射?举几个例子
  • 加载数据库驱动时
  • Spring的IOC容器,根据XML配置文件中的类全限定名动态加载类
  • 工厂方法模式中(如上)

5、说说你对面向对象、封装、继承、多态的理解?

  • 封装:隐藏实现细节,明确标识出允许外部使用的所有成员函数和数据项。 防止代码或数据被破坏。
  • 继承:子类继承父类,拥有父类的所有功能,并且可以在父类基础上进行扩展。实现了代码重用。子类和父类是兼容的,外部调用者无需关注两者的区别。
  • 多态:一个接口有多个子类或实现类,在运行期间(而非编译期间)才决定所引用的对象的实际类型,再根据其实际的类型调用其对应的方法,也就是“动态绑定”。
Java实现多态有三个必要条件:继承、重写、向上转型。
  • 继承:子类继承或者实行父类
  • 重写:在子类里面重写从父类继承下来的方法
  • 向上转型:父类引用指向子类对象
  1. publicclass OOP {
  2.    publicstaticvoid main(String[] args) {
  3.        /*
  4.         * 1. Cat继承了Animal
  5.         * 2. Cat重写了Animal的eat方法
  6.         * 3. 父类Animal的引用指向了子类Cat。
  7.         * 在编译期间其静态类型为Animal;在运行期间其实际类型为Cat,因此animal.eat()将选择Cat的eat方法而不是其他子类的eat方法
  8.         */
  9.        Animal animal = newCat();
  10.        printEating(animal);
  11.    }

  12.    publicstaticvoid printEating(Animal animal) {
  13.        animal.eat();
  14.    }
  15. }

  16. abstractclassAnimal {
  17.    abstractvoid eat();
  18. }
  19. classCatextendsAnimal {
  20.    @Override
  21.    void eat() {
  22.        System.out.println("Cat eating...");
  23.    }
  24. }
  25. classDogextendsAnimal {
  26.    @Override
  27.    void eat() {
  28.        System.out.println("Dog eating...");
  29.    }
  30. }

6、实现不可变对象的策略?比如JDK中的String类。

  • 不提供setter方法(包括修改字段、字段引用到的的对象等方法)
  • 将所有字段设置为final、private
  • 将类修饰为final,不允许子类继承、重写方法。可以将构造函数设为private,通过工厂方法创建。
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。 1)不提供修改可变对象的方法;2)不共享对可变对象的引用。对于外部传入的可变对象,不保存该引用。如要保存可以保存其复制后的副本;对于内部可变对象,不要返回对象本身,而是返回其复制后的副本。

7、Java序列话中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。功能是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

8、==和equals的区别?

== 对于基本类型,比较值是否相等,对于对象,比较的是两个对象的地址是否相同,即是否是指相同一个对象。
equals的默认实现实际上使用了==来比较两个对象是否相等,但是像Integer、String这些类对equals方法进行了重写,比较的是两个对象的内容是否相等。
对于Integer,如果依然坚持使用==来比较,有一些要注意的地方。对于[-128,127]区间里的数,有一个缓存。因此
  1. Integer a = 127;
  2. Integer b = 127;
  3. System.out.println(a == b); // true

  4. Integer a = 128;
  5. Integer b = 128;
  6. System.out.println(a == b); // false

  7. // 不过采用new的方式,a在堆中,这里打印false
  8. Integer a = newInteger(127);
  9. Integer b = 127;
  10. System.out.println(a == b);
对于String,因为它有一个常量池。所以
  1. String a = "gg" + "rr";
  2. String b = "ggrr";
  3. System.out.println(a == b); // true

  4. // 当然牵涉到new的话,该对象就在堆上创建了,所以这里打印false
  5. String a = "gg" + "rr";
  6. String b = newString("ggrr");
  7. System.out.println(a == b);

9、接口和抽象类的区别?

  • Java不能多继承,一个类只能继承一个抽象类;但是可以实现多个接口。
  • 继承抽象类是一种IS-A的关系,实现接口是一种LIKE-A的关系。
  • 继承抽象类可以实现对父类代码的复用,也可以重写抽象方法实现子类特有的功能。实现接口可以为类新增额外的功能。
  • 抽象类定义基本的共性内容,接口是定义额外的功能。
  • 调用者使用动机不同, 实现接口是为了使用他规范的某一个行为;继承抽象类是为了使用这个类属性和行为.

10、给你一个Person对象p,如何将该对象变成JSON表示?

本质是考察Java反射,因为要实现一个通用的程序。实现可能根本不知道该类有哪些字段,所以不能通过get和set等方法来获取键-值。使用反射的getDeclaredFields()可以获得其声明的字段。如果字段是private的,需要调用该字段的 f.setAccessible(true);,才能读取和修改该字段。
  1. import java.lang.reflect.Field;
  2. import java.util.HashMap;

  3. publicclassObject2Json {
  4.    publicstaticclassPerson {
  5.        privateint age;
  6.        privateString name;

  7.        publicPerson(int age, String name) {
  8.            this.age = age;
  9.            this.name = name;
  10.        }
  11.    }

  12.    publicstaticvoid main(String[] args) throwsIllegalAccessException {
  13.        Person p = newPerson(18, "Bob");
  14.        Class<?> classPerson = p.getClass();
  15.        Field[] fields = classPerson.getDeclaredFields();
  16.        HashMap<String, String> map = newHashMap<>();
  17.        for (Field f: fields) {
  18.            // 对于private字段要先设置accessible为true
  19.            f.setAccessible(true);
  20.            map.put(String.valueOf(f.getName()), String.valueOf(f.get(p)));
  21.        }
  22.        System.out.println(map);
  23.    }
  24. }
得到了map,再弄成JSON标准格式就好了。

11、JDBC中sql查询的完整过程?操作事务呢?

  1. @Test
  2. publicvoid fun2() throwsSQLException, ClassNotFoundException {
  3.    // 1. 注册驱动
  4.    Class.forName("com.mysql.jdbc.Driver");
  5.    String url = "jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8";
  6.    // 2.建立连接
  7.    Connection connection = DriverManager.getConnection(url, "root", "admin");
  8.    // 3. 执行sql语句使用的Statement或者PreparedStatment
  9.    Statement statement = connection.createStatement();
  10.    String sql = "select * from stu;";
  11.    ResultSet resultSet = statement.executeQuery(sql);

  12.    while (resultSet.next()) {
  13.        // 第一列是id,所以从第二行开始
  14.        String name = resultSet.getString(2); // 可以传入列的索引,1代表第一行,索引不是从0开始
  15.        int age = resultSet.getInt(3);
  16.        String gender = resultSet.getString(4);
  17.        System.out.println("学生姓名:" + name + " | 年龄:" + age + " | 性别:" + gender);
  18.    }
  19.    // 关闭结果集
  20.    resultSet.close();
  21.    // 关闭statemenet
  22.    statement.close();
  23.    // 关闭连接
  24.    connection.close();
  25. }
ResultSet维持一个指向当前行记录的cursor(游标)指针
  • 注册驱动
  • 建立连接
  • 准备sql语句
  • 执行sql语句得到结果集
  • 对结果集进行遍历
  • 关闭结果集(ResultSet)
  • 关闭statement
  • 关闭连接(connection)
由于JDBC默认自动提交事务,每执行一个update ,delete或者insert的时候都会自动提交到数据库,无法回滚事务。所以若需要实现事务的回滚,要指定 setAutoCommit(false)
  • true:sql命令的提交(commit)由驱动程序负责
  • false:sql命令的提交由应用程序负责,程序必须调用commit或者rollback方法
JDBC操作事务的格式如下,在捕获异常中进行事务的回滚。
  1. try {
  2.  con.setAutoCommit(false);//开启事务…
  3.  ….
  4.  …
  5.  con.commit();//try的最后提交事务
  6. } catch() {
  7.  con.rollback();//回滚事务
  8. }

12、实现单例,有哪些要注意的地方?

就普通的实现方法来看。
  • 不允许在其他类中直接new出对象,故构造方法私有化
  • 在本类中创建唯一一个static实例对象
  • 定义一个public static方法,返回该实例
  1. publicclassSingletonImp {
  2.    // 饿汉模式
  3.    privatestaticSingletonImp singletonImp = newSingletonImp();
  4.    // 私有化(private)该类的构造函数
  5.    privateSingletonImp() {
  6.    }

  7.    publicstaticSingletonImp getInstance() {
  8.        return singletonImp;
  9.    }
  10. }
饿汉模式:线程安全,不能延迟加载。
  1. publicclassSingletonImp4 {
  2.    privatestaticvolatileSingletonImp4 singletonImp4;

  3.    privateSingletonImp4() {}

  4.    publicstaticSingletonImp4 getInstance() {
  5.        if (singletonImp4 == null) {
  6.            synchronized (SingletonImp4.class) {
  7.                if (singletonImp4 == null) {
  8.                    singletonImp4 = newSingletonImp4();
  9.                }
  10.            }
  11.        }

  12.        return singletonImp4;
  13.    }
  14. }
双重检测锁+volatile禁止语义重排。因为 singletonImp4=newSingletonImp4();不是原子操作。
  1. publicclassSingletonImp6 {
  2.    privateSingletonImp6() {}

  3.    // 专门用于创建Singleton的静态类
  4.    privatestaticclassNested {
  5.        privatestaticSingletonImp6 singletonImp6 = newSingletonImp6();
  6.    }

  7.    publicstaticSingletonImp6 getInstance() {
  8.        returnNested.singletonImp6;
  9.    }
  10. }
静态内部类,可以实现延迟加载。
最推荐的是单一元素枚举实现单例。
  • 写法简单
  • 枚举实例的创建默认就是线程安全的
  • 提供了自由的序列化机制。面对复杂的序列或反射攻击,也能保证是单例
  1. publicenumSingleton {
  2.    INSTANCE;
  3.    publicvoid anyOtherMethod() {}
  4. }


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:Java后端开发面试问题总结(上)
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00