1. Java基础
1.1 为什么Java代码可以实现一次编写、到处运行?
在程序运行前,Java源代码(.java)需要经过[编译器]编译成[字节码](.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
1.2 一个Java文件里可以有多个类吗(不含内部类)?
- 一个java文件里可以有多个类,但最多只能有一个被public修饰的类;
- 如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
1.3 说一说你对Java访问权限的了解?
| 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | |
|---|---|---|---|---|
| private | √ | |||
| default | √ | √ | ||
| protected | √ | √ | √ | |
| public | √ | √ | √ | √ |
1.4 介绍一下Java的数据类型?
Java数据类型包括基本数据类型和引用数据类型两大类。
基本数据类型有8个,可以分为4个小类,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。其中,4个整数类型中,int类型最为常用。2个浮点类型中,double最为常用。另外,在这8个基本类型当中,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。
引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为3类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是Java语言不再沿用指针这个说法而已。
扩展阅读
对于基本数据类型,你需要了解每种类型所占据的内存空间,面试官可能会追问这类问题:
- byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1。
- short:2字节(16位),数据范围是 -2^15 ~ 2^15-1。
- int:4字节(32位),数据范围是 -2^31 ~ 2^31-1。
- long:8字节(64位),数据范围是 -2^63 ~ 2^63-1。
- float:4字节(32位),数据范围大约是 -3.410^38 ~ 3.410^38。
- double:8字节(64位),数据范围大约是 -1.810^308 ~ 1.810^308。
- char:2字节(16位),数据范围是 \u0000 ~ \uffff。
- boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。
1.5 int类型的数据范围是多少?
int类型占4字节(32位),数据范围是 -2^31 ~ 2^31-1。
1.6 请介绍全局(成员)变量和局部变量的区别?
Java中的变量分为成员变量和局部变量,它们的区别如下:
成员变量:
- 成员变量是在类的范围里定义的变量;
- 成员变量有默认初始值;
局部变量:
- 局部变量是在方法里定义的变量;
- 局部变量没有默认初始值;
1.7 请介绍一下实例变量的默认值?
实例变量若为引用数据类型,其默认值一律为null。若为基本数据类型,其默认值如下:
- byte:0
- short:0
- int:0
- long:0L
- float:0.0F
- double:0.0
- char:'\u0000'
- boolean:false
1.8 为啥要有包装类?
Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
1.9 说一说自动装箱、自动拆箱的应用场景?
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
1.10 如何对Integer和Double类型判断相等?
Integer、Double不能直接进行比较。整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。
示例代码
Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());
1.11 int和Integer有什么区别,二者在做==运算时会得到什么结果?
int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。
1.12 说一说你对面向对象的理解?
什么是对象:
对象就是事物存在的实体,万物皆对象。举个简单的例子,比如人类就是一个对象,然而对象是有属性和方法的,那么姓名,年龄,身高,体重,性别这些是每个人都有的特征可以概括为属性,当然了我们还会思考,学习,这些行为相当于对象的方法。不过,不同的对象就会有不同的行为。
面向对象:
面向对象是一种优秀的程序设计方法,就是把数据及其操作方法放在一起,将其看成一个整体。对同类对象抽象出其共性,形成类。再答三大特征。
1.13 面向对象的三大特征是什么?
面向对象的程序设计方法具有三个基本特征:封装、继承、多态。其中,封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
扩展阅读
抽象也是面向对象的重要部分。
1.14 封装的目的是什么,为什么要有封装?
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
- 隐藏类的实现细节;
- 限制对成员变量的不合理访问;
- 提高代码的可维护性。
1.15 说一说你对多态的理解?
现实中的事物通常会体现出多种形态,例如学生小明既是学生也是人,即出现了两种形态。Java作为面向对象的语言,同样可以描述一个事物的多种形态,如Student类继承了Person类,即一个Student的对象既是Student,又是Person。
1.16 Java中的多态是怎么实现的?
多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
1.17 Java为什么是单继承,为什么不能多继承?
首先,Java是单继承的,指的是Java中一个类只能有一个直接的父类。Java不能多继承,则是说Java中一个类不能直接继承多个父类。
Java语言之所以摒弃了多继承的这项特征,是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。
准确来说,Java是可以实现"多继承"的。因为尽管一个类只能有一个直接父类,但是却可以有任意多个间接的父类。这样的设计方式,避免了多继承时所产生的混淆。
1.18 说一说重写与重载的区别?
重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为private,则子类不能对其重写。
1.19 Jdk,Jre,JVM的关系?
JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。
1.20 介绍一下Object类中的方法?
Object类提供了如下几个常用方法:
- Class<?> getClass():返回该对象的运行时类。
- boolean equals(Object obj):判断指定对象与该对象是否相等。
- int hashCode():返回该对象的hashCode值。在默认情况下,Object类的hashCode()方法根据该对象的地址来计算。但很多类都重写了Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
- String toString():返回该对象的字符串表示,当程序使用System.out.println()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。Object类的toString()方法返回 运行时类名@十六进制hashCode值 格式的字符串,但很多类都重写了Object类的toString()方法,用于返回可以表述该对象信息的字符串。
1.21 说一说hashCode()和equals()的关系?
hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:
- 如果两个对象相等,则它们必须有相同的哈希码。
- 如果两个对象有相同的哈希码,则它们未必相等。
1.22 为什么要重写hashCode()和equals()?
Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。
equals()方法重写时,通常也要将hashCode()进行重写。
1.23 ==和equals()有什么区别?
- 对于基本类型,==比较的是值;
- 对于引用类型,==比较的是地址;
- equals不能用于基本类型的比较;
- 如果没有重写equals,equals就相当于==;
- 如果重写了equals方法,equals比较的是对象的内容。
1.24 String类有哪些方法?
String类是Java最常用的API,它包含了大量处理字符串的方法,比较常用的有:
- char charAt(int index):返回指定索引处的字符;
- String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;
- String[] split(String regex):以指定的规则将此字符串分割成数组;
- int indexOf(String str):返回子串在此字符串首次出现的索引;
- int lastIndexOf(String str):返回子串在此字符串最后出现的索引。
1.25 说一说String,StringBuffer和StringBuilder?
当对字符串进行修改的时候,特别是字符串对象经常改变的情况下,需要使用 StringBuffer 和 StringBuilder 类。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
1.26 使用字符串时,new和""推荐使用哪种方式?
先看看 "hello" 和 new String("hello") 的区别:
- 当Java程序直接使用 "hello" 的字符串直接量时,JVM将会使用常量池来管理这个字符串;
- 当使用 new String("hello") 时,JVM会先使用常量池来管理 "hello" 直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。
显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。
1.27 两个字符串相加的底层是如何实现的?
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记及答案【扫一扫】 即可免费获取**
如果拼接的都是字符串直接量,则在编译时[编译器]会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。
1.28 String a = "abc"; ,说一下这个过程会创建什么,放在哪里?
JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。
1.29 null和“”的区别?
null是没有地址,"“是有地址但是里面的内容是空的,好比做饭 null说明连锅都没有 而”"则是有锅没米。
1.30 接口和抽象类有什么区别?
接口使用interface修饰;
类可以实现多个接口;
抽象类使用abstract修饰;
抽象类只能单继承;
如果一个类继承了抽象类,①如果实现了所有的抽象方法,子类可以不是抽象类;②如果没有实现所有的抽象方法,子类仍然是抽象类。
1.31 接口中可以有构造函数吗?
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
1.32 谈谈你对面向接口编程的理解?
面向接口编程就是先把客户的业务逻辑线提取出来,作为接口,业务具体实现通过该接口的实现类来完成。当客户需求变化时,只需编写该业务逻辑的新的实现类,通过更改配置文件(例如Spring框架)中该接口的实现类就可以完成需求,不需要改写现有代码,减少对系统的影响。
1.33 遇到过异常吗,如何处理?
在Java中,可以按照如下二个步骤处理异常:
-
捕获异常
将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。
-
处理异常
在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。
java.lang.NullPointerException(空指针异常)
所谓的空指针异常,就是一个指针是空指针(地址都没有分配),你还要去操作它,既然它指向的是空对象,它就不能使用这个对象的方法。比如String s中的s假如为null,你还要用s的方法,比如s.equals( String x);那么就会产生空指针异常。
1.34 说一说Java的异常机制?
关于异常处理:
在Java中,处理异常的语句由try、catch、finally三部分组成。其中,try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行。
关于抛出异常:
当程序出现错误时,系统会自动抛出异常。除此以外,Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理。
1.35 finally是无条件执行的吗?
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
1.36 说一说你对static关键字的理解?
static:就是多个对象共享同一份数据
一个类的不同对象有些共享的数据,这样我们就可以使用static来修饰,一旦使用了static,那么这样的内容不再属于对象,而是属于类的,所以凡是本类的对象,都共享同一份。它可以用来修饰成员变量,修饰成员方法,以及静态代码块。
1.37 static修饰的类能不能被继承?
static修饰的类可以被继承。
1.38 static和final有什么区别?
static:
此变量会被这个类的所有对象所共享,这些对象都可以调用、改变它的值;
无需创建对象也可以调用此方法;
静态方法只可以访问 静态的 属性/变量/方法。
final:
final属性不可被外部更改,而且必须初始化。
1.39 说一说你对Java反射机制的理解?
动态获取类的信息以及动态调用对象的方法就叫反射。
Java程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如 Person p = new Student(); ,这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。
有时,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息,而解决这个问题就是反射。
1.40 介绍一下Java的序列化与反序列化?
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。
若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接口,如包装类、String、Date等。
1.41 进程,线程,多线程?
进程:进程是程序运行时的一个实例。
线程:线程是CPU调度和分派的基本单位,它被包含在进程之中,是进程中的实际运作单位。
多线程:多个线程同时运行。比如说地图导航,当油站过多时,一次查询会很耗时,所以我们可以将一段很长的路线按照路径长度分成若干个条件同时查询,或者说某个程序多个功能同时运转。
1.42 并发和并行?
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
1.43 创建线程有哪几种方式?
创建线程的方式有继承Thread类、实现Runnable接口。
通过继承Thread类(Thread类实现了Runnable接口)来创建并启动线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法。
- 创建Thread子类的实例。
- 调用线程对象的start()方法来启动该线程。
通过实现Runnable接口来创建并启动线程的步骤如下:
- 定义Runnable接口的实现类,并实现该接口的run()方法。
- 创建Runnable实现类的实例。
- 调用线程对象的start()方法来启动该线程。
扩展阅读
采用实现Runnable接口的方式创建多线程的优缺点:
- 线程类只是实现了Runnable接口,还可以继承其他类。
- 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
- 劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
- 优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现Runnable接口的方式来创建多线程。
1.44 说说Thread类的常用方法?
- Thread():构造方法;
- currentThread():返回当前正在执行的线程;
- sleep(long millis):使当前执行的线程睡眠多少毫秒数;
- getId():返回该线程的id;
- getName():返回该线程的名字;
- getPriority():返回该线程的优先级;(默认是5,优先级越高,只能说明抢到线程的概率会大点)
- interrupt():使该线程中断;
- isInterrupted():返回该线程是否被中断;
- setName(String name):设置该线程的名字;
- setPriority(int newPriority):改变该线程的优先级。
1.45 run()和start()有什么区别?
1、start方法用来启动相应的线程;
2、run方法只是thread的一个普通方法,在主线程里执行。
1.46 线程是否可以重复启动,会有什么后果?
只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。
1.47 介绍一下线程的生命周期?
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。当发生如下情况时,线程将会进入阻塞状态:
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:
- 调用sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
线程会以如下三种方式结束,结束后就处于死亡状态:
- run()或call()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。