要须心地收汗马,孔孟行世目杲杲。这篇文章主要讲述10道不得不会的Java基础面试题相关的知识,希望能为你提供帮助。
以下都是java的基础面试题,相信大家都会有种及眼熟又陌生的感觉、看过可能在短暂的面试后又马上忘记了。JavaPub在这里整理这些容易忘记的重点知识及解答,建议收藏,经常温习查阅
。
- instanceof 关键字的作用
- Java自动装箱和拆箱
- 重载和从写区别
- equals与==区别
- 谈谈BIO和BIO区别
- String、StringBuffer、StringBuilder 的区别是什么?
- 泛型是什么,有什么特点
- final 有哪些用法
- 说一下Java注解
- Java创建对象有几种方式
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自动装箱和拆箱。
- Integer变量必须实例化后才能使用,而int变量不需要
- Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。
- Integer的默认值是null,int的默认值是0
文章图片
在面试中:
【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):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
- 方法名必须相同
- 方法的参数列表一定不一样。
- 访问修饰符和返回值类型可以相同也可以不同。
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方法等。在任意子类中定义了重名和同样的参数列表就构成方法重写。
- 方法名必须相同,返回值类型必须相同。
- 参数列表必须相同。
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
- 构造方法不能被重写。
" 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;
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;
。泛型提供了编译时类型安全检测机制,允许在编译时检测到非法的类型。本质是参数化类型。
- 把类型当作是参数一样传递
- < 数据类型> 只能是引用类型
泛型的好处:
- 省略了强转的代码。
- 可以把运行时的问题提前到编译时期。
泛型时提供给javac编译器使用的,用于限定集合的输入类型,让编译器在源代码级别上,避免向集合中插入非法数据。但编译器编译完带有泛型的java程序后,生成的class文件中不再带有泛型信息,以此使程序运行效率不受影响,这个过程称为擦除。
JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法; 处理机制是通过类型擦除,擦除规则:
- 若泛型类型没有指定具体类型,用Object作为原始类型;
- 若有限定类型<
T exnteds XClass >
,使用XClass作为原始类型;
- 若有多个限定< T exnteds XClass1 & XClass2 > ,使用第一个边界类型XClass1作为原始类型;
final修饰一个类
当 final 关键字用来修饰一个类的时候,表明这个类不能有任何的子类,也就是说这个类不能被继承。
final类中的所有成员方法都会被隐式地指定为final方法,也就是说一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写。
public final class 类名称 // ...
final修饰一个方法
当 final 关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。
修饰符 final 返回值类型 方法名称(参数列表) // 方法体
注意:对于类、方法来说,abstract 关键字和 final 关键字不能同时使用,因为矛盾。
final修饰一个局部变量
一旦使用 final 用来修饰局部变量,那么这个变量就不能进行更改「一次赋值,终生不变」。
- 对于基本类型来说,不可变说的是变量当中的数据不可改变;
- 对于引用类型来说,不可变说的是变量当中的地址值不可改变。
对于成员变量来说,如果使用 final 关键字修饰,那么这个变量也照样是不可变。
- 由于成员变量具有默认值,所以用了 final 之后必须手动赋值,不会再给默认值了;
- 对于 final 的成员变量,要么使用直接赋值,要么通过构造方法赋值,必须二者选其一;
- 必须保证类当中所有重载的构造方法都最终会对 final 的成员变量进行赋值。
注解大致分为以下三种:
- Java原生注解 如@Override,@Deprecated 等。大多用于 [标记] 和 [检查] 。
- 第三方注解,如 Spring、Mybatis等定义的注解(@Controller,@Data)。
- 自定义注解。
- @Target
- @Retention
- @Documented
- @Inherited
文章图片
一般比较常用的有 @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资源";
很简单的俩个接口,没添加拦截器之前成功访问
文章图片
实现 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("/**");
拦截成功如下
文章图片
在 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则不用登录。
文章图片
文章图片
场景二:自定义注解+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项目,输入访问地址
文章图片
文章图片
有些面试官喜欢问,注解三要素是哪些:
- 注解声明、
- 使用注解的元素、
- 操作注解使其起作用(注解处理器)
文章图片
使用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();
推荐阅读
- Flutter 专题40 日常问题小结#yyds干货盘点#
- #私藏项目实操分享#分布式技术专题「OSS中间件系列」Minio的文件服务的存储模型及整合Java客户端访问的实战指南
- Springboot2+Quartz+debug源码教程#yyds干货盘点#
- 什么是 APM 系统(如何设计与实现?#yyds干货盘点#)
- #私藏项目实操分享#?Alibaba中间件技术系列「EasyExcel实战案例」实战研究一下EasyExcel如何从指定文件位置进行读取数据
- #yyds干货盘点#Springboot——Shiro(安全框架)学习笔记
- 学习Java必备的基础知识打卡12.19,要想学好必须扎实基本功(?建议收藏)#yyds干货盘点#
- #yyds干货盘点# 数据结构与算法之单链表
- #yyds干货盘点# 滚雪球学 Python 之怎么玩转时间和日期库