java基础面试题总结
java基础面试题总结
目录
前言
1. JVM vs JDK vs JRE的了解
2. 谈谈你对编程、编译、运行的理解
3. 什么是字节码?采用字节码的好处是什么?
5. java中的注解有几种,分别是什么?
6. 字符型常量和字符串常量
7.标识符和关键字的认识
8. 泛型,类型擦除和泛型的原理
9. == 和equals的了解和区别
10. hashcode()和equals()方法的认识
11. final关键字
12. 接口和抽象类的区别
13. List集合和Set集合的区别
14. ArrayList和LinkedList的区别?
15. HashMap和HashTable有什么区别?其底层实现是什么?
16. Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
17. HashMap的扩容机制原理?
18. 基本数据类所占字节数
19. 自动装箱和拆箱以及原理?
20. 数据类型的封装类,以及中间的区别
21. 包装类型的缓存机制你了解吗?
22. String, StringBuffer 以及StringBuilder三者的区别
23. 谈谈你对String的了解?
24. 方法有几种?什么是方法的返回值?
25. 静态方法的特殊性?
前言:Java八股文作为面试中的常见题型,首要作用是帮助面试官快速评估求职者的Java基础能力是否扎实。通过这些问题,面试官可以判断求职者是否具备Java开发所需的基本素养和技能。虽然现在的场景题也很重要,但是… …
1. JVM vs JDK vs JRE的了解
参考答案:
JVM: java Virtual Machine java 虚拟机,是运行java字节码的虚拟机,它针对不同的系统有着不同的实现,目的就是在不同的系统上运行相同的字节码文件时,有相同的结果。同时字节码和不同系统的jvm也是java“一次编译,随处运行”的关键所在。
JDK: Java Develpment Kit java 开发⼯具 ,是java的开发工具,也就是java sdk (java软件开发工具包),它包含jvm和jre,其中含有类似于javac(编译器)等的开发工具
JRE: Java Runtime Environment java运⾏时环境 ,是java的运行时环境,它包含jvm,就是运行一些已经编译好的java程序
简单说就是:
JDK是java的开发工具包
JDK = JVM+核心类库+开发工具(如javac编译器、java运行时、调试器等)+JRE
而JRE(Java Runtime Environment):提供了Java程序运行的环境,包括JVM(Java虚拟机)和核心类库
编程 :程序员编写的源代码,就是.Java文件,.py文件
编译 :机器只认识0011的机器语言,把.java,.c,.py的代码做转化让机器认识的过程
运行:让机器执行编译后的指令
字节码 :是介于java源代码和机器码之间的代码形式,也就是jvm可以理解的代码,是一种二进制代码,它只面向jvm虚拟机,比如说java的.class文件就是字节码文件
好处: 在一定程度上解决了传统解释型语言执行效率低的问题(有jit编译器;减少解释器对源代码的直接解析工作),同时又保留了解释型语言可移植的特点(只面向jvm)。采用字节码最大的优点就是,它不是只针对一种特定的机器,而是面向JVM虚拟机,比如Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。同时它的效率也比较高(即时编译器(JIT)可以将字节码编译成高效的本地机器码)。
java从源代码到运行的过程,源代码->.java文件->javac编译->.class字节码文件->解释器(JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译),当一次编译过后,字节码对应的机器码就会被保留下来,下次可以直接使用->解释成机器可以理解的代码->运行,所以java既有编译型语言的特点,又有解释型语言的特征
补充:
编译型 : 编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型 : 解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
注释分为单行注释、多行注释和文档注释,主要是使我们可以看懂以前写过相对复杂的代码,或者就是进行团队开发时,便于他人理解你写的代码。
// /* */ /** */
1. 字符型常量: 字符型常量是由单个字符组成的,用单引号括起来。例如:‘A’, ‘b’, '1’等。在内存中,字符型常量被表示为对应的 Unicode 编码值
2. 字符串常量: 字符串常量是由多个字符组成的,用双引号括起来。例如:“Hello”, "World"等。在内存中,字符串常量以字符数组的形式存在,并且每个字符都有一个对应的 Unicode 编码值,
区别如下:
1. 定义方式不同: 字符型常量使用单引号括起来,字符串常量使用双引号括起来。
2. 数据类型不同: 字符型常量属于 char 类型,字符串常量属于 String 类型。
3. 长度不同: 字符型常量只能包含一个字符,而字符串常量可以包含任意数量的字符。
4. 内存表示不同: 字符型常量在内存中占用 2 个字节,字符串常量在内存中以字符数组的形式存在。
5. 操作方法不同: 字符型常量可以进行一些基本的字符操作,如比较、转换大小写等;字符串常量则可以进行更复杂的字符串操作,如连接、截取、替换等。
标识符: 就是我们在编写程序时,要给大量的类、方法、变量取一个名字,简单说标识符就是名字
命名规则: 1、不能是关键字;2、不能以数字开头3、只能由数字、字母、下划线和美元符号($)或中元符号(¥)组成4、区分大小写
关键字: 就是java本身已经赋予了它特定的含义,只能用在指定的地方,关键字就是赋予特殊含义标识符
泛型定义: 泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)。
注意: 不能在静态方法、静态代码块等静态内容中使用泛型的类型参数。
通配符: 问号通配符 ? 表示未知类型。通配符可用于参数、字段、局部变量和返回类型。可以近似的理解为泛型的泛型。
上界限定通配符: <? extends E>,表示只接受E类型及其子类型。
下界限定通配符: <? super E>, 表示只接受E类型及其父类型。
泛型的原理: Java 语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。
类型擦除: 泛型信息只存在于编译前,编译后的字节码中是不包含泛型中的类型信息的。因此,编译器在编译时去掉类型参数,叫做类型擦除。
未指定上界的泛型类型会以Object类型替换
已指定上界的泛型类型会以上界类型替换
泛型中extends和super的区别
- <? extends T>表示包括T在内的任何T的⼦类
- <? super T>表示包括T在内的任何T的⽗类
1.类型:= = 是一个运算符,而 equals() 是一个方法。
2.比较对象:= = 比较的对象可以是基本类型也可以是引用类型,而equals()比较的是引用数据类型,不能比较基本数据类型,会报错
3.比较内容:= =针对基本类型时比较的是对象的值,针对引用类型比较的是对象的指向的内存地址是否相等。equals针对的比较对象是引用类型,重写后可以按照自己的方式进行比较。 在Java中Object对象是所有对象的父类,所以每个类都会有个equals的方法,如果你没有重写它那些它与==的效果是一样的,可以通过源代码查看。
总结: = =是一个运算符,用于比较两个对象的引用是否相同,即它们是否指向内存中的相同位置。
equals 是一个方法,通常在Object类中定义,它用于比较两个对象的内容是否相等。默认情况下,equals方法执行与= =相同的引用比较,但它可以被子类重写以提供自定义的相等性逻辑。
equals()保证可靠性,而hashcode保证性能
equals 和 hashCode 都可用来判断两个对象是否相等,但是二者有区别
equals 可以保证比较对象是否是绝对相等,即「equals 保证可靠性」
hashCode 用来在最快的时间内判断两个对象是否相等,可能有「误判」,即「hashCode 保证性能」
两个对象 equals 为 true 时,要求 hashCode 也必须相等
两个对象 hashCode 为 true 时,equals 可以不等(如发生哈希碰撞时)
Hashcode: hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
为什么要有hashcode()?
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其假如操作成功。这样就大大减少了equals的次数,相应就大大提高了执行速度。
为什么需要同时重写 hashCode() 和 equals()?
当你尝试将对象添加到 HashSet、HashMap 等哈希的集合时,会首先调用对象的 hashCode() 方法来确定对象应该存储在哈希表中的哪个位置(桶位)。如果两个对象通过 equals() 方法比较相等,但它们的 hashCode() 方法返回不同的值,那么这些对象可能会被存储在不同的桶位中,这会导致集合中的行为不一致,比如无法正确地移除或查找元素。
因此,当你重写 equals() 方法时,也应该重写 hashCode() 方法,以确保 equals() 方法认为相等的对象具有相同的哈希码。
hashCode 的「误判」指的是什么
同一个对象的 hashCode 一定相等。
不同对象的 hashCode 也可能相等,这是因为 hashCode 是根据地址 hash 出来的一个 int 32 位的整型数字,相等是在所难免。
- final修饰类不能被继承;
- 修饰方法,不能被重写;
- 修饰成员变量:
a. 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
b. 如果final修饰的是成员变量,可以在⾮静态初始化块、声明该变量时或者构造器中执⾏初始值。
c. 修饰局部变量:
系统不会为局部变量进⾏初始化,局部变量必须由程序员显示初始化。因此使⽤final修饰局部变量时,即可以在定义时指定默认值(后⾯的代码不能对变量再赋值),也可以不指定默认值,⽽在后⾯的代码 中对final变量赋初值(仅⼀次)- 修饰基本类型数据和引⽤类型数据:
如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象,即地址值不能改变了,但是引⽤的值是可变,对象的值可以改变,比如说数组,不能将数组等于null,但是可以改变数组的值
为什么局部内部类和匿名内部类只能访问局部final变量?
⾸先需要知道的⼀点是: 内部类和外部类是处于同⼀个级别的,内部类不会因为定义在⽅法中就会随着 ⽅法的执⾏完毕就被销毁。
这⾥就会产⽣问题:当外部类的⽅法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有 没有⼈再引⽤它时,才会死亡)。这⾥就出现了⼀个⽭盾:内部类对象访问了⼀个不存在的变量。为了解决这个问题,就将局部变量复制了⼀份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延⻓了局部变量的⽣命周期,将局部变量复制为内部类的成员变量时,必须保证这两个变量是⼀样的,也就是如果我们在内部类中修改了成员变量,⽅法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和⽅法的局部变量的⼀致性。这实际上也是⼀种妥协。使得局部变量与内部类内建⽴的拷⻉保持⼀致。
对于类、方法来说,为什么abstract关键字和final关键字不能同时使用?
有抽象方法的abstract类被继承时,其中的方法必须被子类Override,而final不能被Override,互相矛盾。
- 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
- 抽象类只能继承一个,接口可以实现多个。
接口设计的目的是对类的行为进行约束,可以规定类必须有哪些行为,对其内部的具体实现不做要求,抽象类不一定具有抽象方法,有抽象方法的类一定是抽象类。
抽象类则是代码的复用,当不同的类具有某些相同的⾏为(记为⾏为集合A),且其中⼀部分⾏为的实现⽅式⼀致时(B),可以让这些类都派⽣于⼀个抽象类。这个类中实现了B,避免让所有的⼦类来实现B,这就达到了代码复⽤的⽬的。对于A减B的这一部分,让子类去自己实现,正是因为A-B在这⾥没有实现,所以抽象类不允许实例化出来(否则当调⽤到A减B时,⽆法执⾏)。
举例:抽象类是对类本质的抽象,表达的是 is a 的关系,⽐如: BMW is a Car 。抽象类包含并实现⼦类的通⽤特性,将⼦类存在差异化的特性进⾏抽象,交由⼦类去实现。
⽽接⼝是对⾏为的抽象,表达的是 like a 的关系。⽐如: Bird like a Aircraft (像⻜⾏器⼀样可以⻜),但其本质上 is a Bird 。接⼝的核⼼是定义⾏为,即实现类可以做什么,⾄于实现类主体是谁、是如何实现的,接⼝并不关⼼。
● List:有序,按对象进⼊的顺序保存对象,可重复,允许多个Null元素对象,可以使⽤Iterator取出 所有元素,在逐⼀遍历,还可以使⽤get(int index)获取指定下标的元素
● Set:⽆序,不可重复,最多允许有⼀个Null元素对象,取元素时只能⽤Iterator接⼝取得所有元素,在逐⼀遍历各个元素
- 底层实现
ArrayList 是动态数组的数据结构实现
LinkedList 是双向链表的数据结构实现- 操作效率
查找:
a. ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】LinkedList不支持下标查询
b. 查找(未知索引,知道值):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)
新增和删除:
a. ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
b. LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)- 内存空间
1.ArrayList底层是数组,内存连续,节省内存
2.LinkedList 是双向链表需要存储数据,和两个指针,更占用内存- 是否是线程安全
ArrayList和LinkedList都不是线程安全的
如果需要保证线程安全,有两种方案:
1.在方法内使用,局部变量则是线程安全的
2.使用线程安全的ArrayList和LinkedList
- 另外ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以LinkedList还可以当做队列来使⽤;
15. HashMap和HashTable有什么区别?其底层实现是什么?
区别 :
- HashMap⽅法没有synchronized修饰,线程⾮安全,HashTable线程安全;
- HashMap允许key和value为null,⽽HashTable不允许
底层实现: 数组+链表实现,jdk8开始链表⾼度到8、数组⻓度超过64,链表转变为红⿊树,元素以内部 类Node节点存在
原理:
●计算key的hash值,⼆次hash然后对数组⻓度取模,对应到数组下标;
● 如果没有产⽣hash冲突(下标位置没有元素),则直接创建Node存⼊数组;
●如果产⽣hash冲突,先进⾏equal⽐较,相同则取代该元素,不同,则判断链表⾼度插⼊链表,链表⾼度达到8,并且数组⻓度到64则转变为红⿊树,⻓度低于6则将红⿊树转回链表;
●key为null,存在于下标0的位置;
16. Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率;
- 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法;
- 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法,节省CPU资源;
1.7版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
- 将元素添加到新数组中去
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表或红⿊树
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应
位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的
对应位置- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
基本数据类型一共有4类8种
整型:字节byte(1)、short(2)、 int(4)、long(8)
浮点型:float(4)、double(8)
字符型:字符char(2)
布尔型:boolean并不是占用一个字节,具体要看虚拟机实现是否按照规范来,1个字节、4个字节都是有可能的,可能被编译成int类型。
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作
为了对基本数据类型进行更多方便的操作,java就针对每一种基本数据类型提供了对应的类类型,也就是基本数据类型相对应的封装类
区别:
- 默认值:包装类不赋值的话默认为null,但是基本数据类型有默认值
- 应用:包装类可以用于泛型,但是基本数据类型不可以,包装类还提供了一些列的方法可以使用
- 性能:一般来说基本数据类型占用的内存小于包装类型
- 存储方式:基本数据类型如果未被static修饰存在堆中,要是被statis修饰存放在栈中,但是引用数据类型存储在堆内存中
局部变量(方法内部声明的变量)通常存储在Java虚拟机的栈中,而成员变量(类内部声明的变量)如果没有被static修饰,则存储在堆中。
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
对于整型包装类之间的比较用equals()方法
22. String, StringBuffer 以及StringBuilder三者的区别
- 可变性:首先他们都是被final修饰的类,都是不可被继承,不过从可变性上来说,String 我们刚刚说到了,是不可变的,而StringBuffer 和 StringBuilder 可变的,这是内部结构导致的,StringBuffer 和StringBuilder 内部放数据的数组没有被final修饰。
- 安全性:
String 不可变,是线程安全的
StringBuilder 不是线程安全的,因为内部并没有使用任何的安全处理
StringBuffer 是线程安全的,内部使用 synchronized 进行同步- 性能:StringBuilder和StringBuffer在内部维护了字符数组,可以直接在数组上进行修改,而String每次修改都会生成新的对象。StringBuilder和StringBuffer性能更好,但是StringBuilder它内部没有同步的机制,在单线程中的性能更好。
String 被声明为 final,因此它不可被继承。在 Java 8 中,String 内部使用 char 数组存储数据,并且声明为 final,这意味着 value(数组名为value) 数组初始化之后就不能再引用其它数组,String 内部也没有改变 value 数组的方法,因此可以保证 String 不可变。
继续深挖 说说看不可变的好处?
可以从多方面回答,这里列举了几个:
- 首先是不可变自然意味着安全,当String 作为参数引用的时候,不可变性可以保证参数不可变。
- 其次是可以缓存 hash 值,实际上,我们开发的时候经常会用来当做map的key,不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
- 最后自然是String Pool 的需要,如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用,而自然只有 String 是不可变的,才可能使用 String Pool。如果是可变的,那么 String Pool也就无法被设计出来了。
种类: 1.无参数无返回值;2.无参数有返回值;3.有参数无返回值;4.有参数有返回值
方法的返回值: 当我们执行完一个方法后返回的结果(前提是这个方法有返回值),我们可以拿这个方法的返回值去做其他的操作
1. 静态方法不能调用非静态成员
原因1: 静态方法属于类,在类加载的时候就已经为其分配内存,可以通过类名直接访问;而非静态成员属于实例对象,只有在对象实例化之后才存在,要通过类的实例化对象访问
原因2: 因此在类的非静态成员不存在的时候,类的静态成员就已经存在,因此调用在内存中还不存在的非静态成员是不合法的
2. 属于类,而不是对象
3. 静态方法可以直接使用类名+静态方法名调用,不会产生该类的对象
4. 当一个对象为null时,可以调用静态方法,但是调用实例方法时会出现空指针异常。
冲了,兄弟们!!!
未完… …
原文地址:https://blog.csdn.net/liusaidh/article/details/142283372
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!