10道不得不会的Java基础面试题

要须心地收汗马,孔孟行世目杲杲。这篇文章主要讲述10道不得不会的Java基础面试题相关的知识,希望能为你提供帮助。
以下都是java的基础面试题,相信大家都会有种及眼熟又陌生的感觉、看过可能在短暂的面试后又马上忘记了。JavaPub在这里整理这些容易忘记的重点知识及解答,建议收藏,经常温习查阅

  1. instanceof 关键字的作用
  2. Java自动装箱和拆箱
  3. 重载和从写区别
  4. equals与==区别
  5. 谈谈BIO和BIO区别
  6. String、StringBuffer、StringBuilder 的区别是什么?
  7. 泛型是什么,有什么特点
  8. final 有哪些用法
  9. 说一下Java注解
  10. Java创建对象有几种方式
@[toc]
1. instanceof 关键字的作用instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
boolean result = obj instanceof class

当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意一点:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
obj 必须为引用类型,只能作为对象的判断,不能是基本类型。
int i = 0; System.out.println(i instanceof Integer); //编译不通过 System.out.println(i instanceof Object); //编译不通过

源码参考:JavaSE 8 instanceof 的实现算法:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.instanceof
2. Java自动装箱和拆箱什么是装箱拆箱,这里不做源码层面解读,源码解读在JavaPub公众号发出。这里通过讲解 int 和 Interger 区别,解答Java自动装箱和拆箱。
  1. Integer变量必须实例化后才能使用,而int变量不需要
  2. Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。
  3. Integer的默认值是null,int的默认值是0
Java中8种基本数据类型。左边基本类型,右边包装类型。
10道不得不会的Java基础面试题

文章图片

在面试中:
【10道不得不会的Java基础面试题】下面这段代码的输出结果是什么?
public class Main public static void main(String[] args) Integer i1 = 100; Integer i2 = 100; Integer i3 = 200; Integer i4 = 200; System.out.println(i1==i2); System.out.println(i3==i4); //true //false

输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:
public static Integer valueOf(int i) if(i > = -128 & & i < = IntegerCache.high) return IntegerCache.cache[i + 128]; else return new Integer(i);

private static class IntegerCache static final int high; static final Integer cache[]; static final int low = -128; // high value may be configured by property int h = 127; if (integerCacheHighPropValue != null) // Use Long.decode here to avoid invoking methods that // require Integers autoboxing cache to be initialized int i = Long.decode(integerCacheHighPropValue).intValue(); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - -low); high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); private IntegerCache()

上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。
注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。< br/> Double、Float的valueOf方法的实现是类似的(没有缓存数值,这里的数值想想都有很多,不适合缓存)。
3. 重载和重写区别重载和重写是一个特别好理解的概念,这里说一个通俗的解答方式
重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
  1. 方法名必须相同
  2. 方法的参数列表一定不一样。
  3. 访问修饰符和返回值类型可以相同也可以不同。
其实简单而言:重载就是对于不同的情况写不同的方法。 比如,同一个类中,写不同的构造函数用于初始化不同的参数。
public class JavaPubTest public void out() System.out.println("参数"+null); //参数数目不同 public void out(Integer n) System.out.println("参数"+n.getClass().getName()); //参数类型不同 public void out(String string) System.out.println("参数"+string.getClass().getName()); public void out(Integer n ,String string) System.out.println("参数"+n.getClass().getName()+","+string.getClass().getName()); //参数顺序不同 public void out(String string,Integer n) System.out.println("参数"+string.getClass().getName()+","+n.getClass().getName()); public static void main(String[] args) JavaPubTest javaPubTest = new JavaPubTest(); javaPubTest.out(); javaPubTest.out(1); javaPubTest.out("string"); javaPubTest.out(1,"string"); javaPubTest.out("string",1);

重写(Overriding)发生在父类子类之间,比如所有类都是继承与Object类的,Object类中本身就有equals、hashcode、toString方法等。在任意子类中定义了重名和同样的参数列表就构成方法重写。
  1. 方法名必须相同,返回值类型必须相同。
  2. 参数列表必须相同。
  3. 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
  4. 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  5. 构造方法不能被重写。
4. equals与==区别" ==" 是判断两个变量或实例是不是指向同一个内存空间。
" equals" 是判断两个变量或实例所指向的内存空间的值是不是相同。
除了这俩点,这个问题大概率会引出以下问题:
通过上面俩条我们知道 " equals" 是判断两个变量或实例所指向的内存空间的值是不是相同。 但是一些特殊场景,我们需要对比俩个对象是否相等,例如:User user1 = new User(); User user2 = new User(); user1 和 user2 对比。这是我们就需要重写 equals 方法。
所以可以通过重写equals()方法来判断对象的值是否相等,但是有一个要求:equals()方法实现了等价关系,即:
  • 自反性:对于任何非空引用x,x.equals(x)应该返回true;
  • 对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true;
  • 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true;
  • 一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果;
  • 非空性:对于任意非空引用x,x.equals(null)应该返回false;
到这里也是一个很正常的操作,但是当我们要用到 HashSet 等集合时。存储的对象我们需要用 hashcode 判断对象是否存在,如果使用 Object 默认的hashcode方法,那我们同样属性的俩个用户一定是不相等的(例如下面user3、user4),因为内存地址不同,这并不符合我们的业务,所以决定了重写 hashcode 的必要性。
User user3 = new User("JavaPub", "man", "1996-08-28") User user4 = new User("JavaPub", "man", "1996-08-28")

5. 谈谈NIO和BIO区别网上看到一个例子:
一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )2. 每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 )很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。

6. String、StringBuffer、StringBuilder 的区别是什么?String是Immutable类的典型实现,被声明为 final class,除了hash这个属性其它属性都声明为final。它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
StringBuffer、StringBuilder就是解决String的这个性能问题。
StringBuffer 是线程安全的,本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上synchronized。
StringBuffer 线程不安全,但是性能更好。
7. 泛型是什么,有什么特点泛型在编码中有非常广泛的使用(jdk5引入),你一定经常能见到类似这种写法 & lt; T& gt;
泛型提供了编译时类型安全检测机制,允许在编译时检测到非法的类型。本质是参数化类型。
  1. 把类型当作是参数一样传递
  2. < 数据类型> 只能是引用类型
泛型:就是一种不确定的数据类型。
泛型的好处:
  1. 省略了强转的代码。
  2. 可以把运行时的问题提前到编译时期。
泛型擦除:
泛型时提供给javac编译器使用的,用于限定集合的输入类型,让编译器在源代码级别上,避免向集合中插入非法数据。但编译器编译完带有泛型的java程序后,生成的class文件中不再带有泛型信息,以此使程序运行效率不受影响,这个过程称为擦除。
JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法; 处理机制是通过类型擦除,擦除规则:
  • 若泛型类型没有指定具体类型,用Object作为原始类型;
  • 若有限定类型< T exnteds XClass > ,使用XClass作为原始类型;
  • 若有多个限定< T exnteds XClass1 & XClass2 > ,使用第一个边界类型XClass1作为原始类型;
8. final 有哪些用法final关键字有四个常见用法。
final修饰一个类
当 final 关键字用来修饰一个类的时候,表明这个类不能有任何的子类,也就是说这个类不能被继承。
final类中的所有成员方法都会被隐式地指定为final方法,也就是说一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写。
public final class 类名称 // ...

final修饰一个方法
当 final 关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。
修饰符 final 返回值类型 方法名称(参数列表) // 方法体

注意:对于类、方法来说,abstract 关键字和 final 关键字不能同时使用,因为矛盾。
final修饰一个局部变量
一旦使用 final 用来修饰局部变量,那么这个变量就不能进行更改「一次赋值,终生不变」。
  1. 对于基本类型来说,不可变说的是变量当中的数据不可改变;
  2. 对于引用类型来说,不可变说的是变量当中的地址值不可改变。
final修饰一个成员变量
对于成员变量来说,如果使用 final 关键字修饰,那么这个变量也照样是不可变。
  1. 由于成员变量具有默认值,所以用了 final 之后必须手动赋值,不会再给默认值了;
  2. 对于 final 的成员变量,要么使用直接赋值,要么通过构造方法赋值,必须二者选其一;
  3. 必须保证类当中所有重载的构造方法都最终会对 final 的成员变量进行赋值。
9. 说一下Java注解在Java编程中,注解非常常见,注解的本质是什么?
注解大致分为以下三种:
  1. Java原生注解 如@Override,@Deprecated 等。大多用于 [标记] 和 [检查] 。
  2. 第三方注解,如 Spring、Mybatis等定义的注解(@Controller,@Data)。
  3. 自定义注解。
Java原生除了提供基本注解,还提供了 meta-annotation(元注解)。这些类型和它们所支持的类在java.lang.annotation包中可以找到。
  1. @Target
  2. @Retention
  3. @Documented
  4. @Inherited
10道不得不会的Java基础面试题

文章图片

一般比较常用的有 @Target,@Retention。@Target表示这个注解可以修饰那些地方(比如类、方法、成员变量),@Retention 主要是设置注解的生命周期。
注解有一个非常常见的使用场景,大家可以用这个来理解学习。
场景一:自定义注解+拦截器 实现登录校验
实现功能:
接下来,我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。
首先定义一个LoginRequired注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginRequired

然后写两个简单的接口,访问sourceA,sourceB资源
@RestController public class IndexController @GetMapping("/sourceA") public String sourceA() return "你正在访问sourceA资源"; @GetMapping("/sourceB") public String sourceB() return "你正在访问sourceB资源";

很简单的俩个接口,没添加拦截器之前成功访问
10道不得不会的Java基础面试题

文章图片

实现 spring 的 HandlerInterceptor 类先实现拦截器,但不拦截,只是简单打印日志,如下:
public class SourceAccessInterceptor implements HandlerInterceptor @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception System.out.println("进入拦截器了"); return true; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception

实现spring类 WebMvcConfigurer,创建配置类把拦截器添加到拦截器链中
@Configuration public class InterceptorTrainConfigurer implements WebMvcConfigurer @Override public void addInterceptors(InterceptorRegistry registry) registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");

拦截成功如下
10道不得不会的Java基础面试题

文章图片

在 sourceB 方法上添加我们的登录注解 @LoginRequired
@RestController public class IndexController @GetMapping("/sourceA") public String sourceA() return "你正在访问sourceA资源"; @LoginRequired @GetMapping("/sourceB") public String sourceB() return "你正在访问sourceB资源";

简单实现登录拦截逻辑
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception System.out.println("进入拦截器了"); // 反射获取方法上的LoginRequred注解 HandlerMethod handlerMethod = (HandlerMethod)handler; LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class); if(loginRequired == null) return true; // 有LoginRequired注解说明需要登录,提示用户登录 response.setContentType("application/json; charset=utf-8"); response.getWriter().print("你访问的资源需要登录"); return false;

运行成功,访问sourceB时需要登录了,访问sourceA则不用登录。
10道不得不会的Java基础面试题

文章图片

10道不得不会的Java基础面试题

文章图片

场景二:自定义注解+AOP 实现日志打印
先导入切面需要的依赖包
< dependency> < groupId> org.springframework.boot< /groupId> < artifactId> spring-boot-starter-aop< /artifactId> < /dependency>

定义一个注解@MyLog
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog

定义一个切面类,见如下代码注释理解:
@Aspect // 1.表明这是一个切面类 @Component public class MyLogAspect // 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名 // 切面最主要的就是切点,所有的故事都围绕切点发生 // logPointCut()代表切点名称 @Pointcut("@annotation(com.javapub.blog.MyLog)") public void logPointCut(); // 3. 环绕通知 @Around("logPointCut()") public void logAround(ProceedingJoinPoint joinPoint) // 获取方法名称 String methodName = joinPoint.getSignature().getName(); // 获取入参 Object[] param = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); for(Object o : param) sb.append(o + "; "); System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString()); // 继续执行方法 try joinPoint.proceed(); catch (Throwable throwable) throwable.printStackTrace(); System.out.println(methodName + "方法执行结束");

在步骤二中的IndexController写一个sourceC进行测试,加上我们的自定义注解:
@MyLog @GetMapping("/sourceC/source_name") public String sourceC(@PathVariable("source_name") String sourceName) return "你正在访问sourceC资源";

启动springboot web项目,输入访问地址
10道不得不会的Java基础面试题

文章图片

10道不得不会的Java基础面试题

文章图片

有些面试官喜欢问,注解三要素是哪些:
  1. 注解声明、
  2. 使用注解的元素、
  3. 操作注解使其起作用(注解处理器)
10. Java创建对象有几种方式Java中有5种创建对象的方式,下面给出它们的例子
10道不得不会的Java基础面试题

文章图片

使用new关键字
User user = new User();

使用Class类的newInstance方法
我们也可以使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。
Employee emp = (Employee) Class.forName("org.javapub.blog.Employee").newInstance(); 或者 Employee emp2 = Employee.class.newInstance();

使用Constructor类的newInstance方法
和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
Constructor< Employee> constructor = Employee.class.getConstructor(); Employee emp3 = constructor.newInstance();

使用clone方法
无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。
要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。
Employee emp4 = (Employee) emp3.clone();

使用反序列化
当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象。在反序列化时,jvm创建对象并不会调用任何构造函数。
为了反序列化一个对象,我们需要让我们的类实现Serializable接口
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj")); Employee emp5 = (Employee) in.readObject();


    推荐阅读