基础概念
Java 语言有哪些特点
- 简单易学;
- 面向对象(封装,继承,多态);
- 跨平台,一次编写,随处运行( Java 虚拟机实现平台无关性);
- 支持多线程;
- 可靠性(具备异常处理和自动内存管理机制);
- 安全性(如访问权限修饰符、限制程序直接访问操作系统资源);
- 生态强(拥有许多开发框架,社区完善)
- 高效性(Just In Time 编译器等技术的优化的);
- 支持网络编程并且很方便;
- 编译与解释并存;
JVM、JDK、JRE的区别
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JDK 是 Java开发工具,它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
JRE 是 Java 运行时环境。包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。它不能用于创建新程序。
什么是字节码,采用字节码的好处
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。
高效性:Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
跨平台:字节码不针对一种特定的机器,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
JIT和AOT
JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。(编译,是编译与解释共存的语言)
AOT(Ahead of Time Compilation)编译模式,直接将字节码编译成机器码,这样避免了 JIT 预热等各方面的开销。
JIT支持动态代理,AOT不支持
为什么说 Java 语言“编译与解释并存”
- 编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。编译语言的执行速度比较快,开发效率比较低。 C、C++、Go、Rust
- 解释型:解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。 Python、JavaScript、PHP
因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
Java 和 C++ 的区别是什么
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载。
JDK8的新特性
- 引入Lambda表达式:允许把函数作为一个方法的参数。
- 方法与构造函数引用:允许使用 : : 关键字来传递方法或者构造函数引用
- 接口默认方法和静态方法:支持在接口中定义默认方法和静态方法, 默认方法可以被接口实现引用。
- JVM新特性:使用metaSpace代替永久区
- Date API:在包java.time下包含了一组全新的时间日期API(Clock,LocalTime,LocalDate等)
- 多重注解:允许把同一个类型的注解使用多次,只需要给该注解标注一下@Repeatable即可。
- HashMap变化:将链表方式修改成链表或者红黑树的形式,JDK1.7存储使用Entry数组, JDK8使用Node或者TreeNode数组存储,修改resize的过程,解决JDK7在resize在并发场景下死锁的隐患(JDK8插入从链表尾部插入)
- ConcurrentHashMap的实现:在 jdk 1.7 中,ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成,采取分段锁来保证安全性。JDK1.8 中,用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作
基础语法
标识符和关键字的区别是什么
编写程序的时需要为程序、类、变量、方法等取名字,于是就有了 标识符 。
标识符就是一个名字
关键字是被赋予特殊含义的标识符
final 关键字的作用
用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承
- 修饰的变量是基本数据类型则值不能改变
- 修饰的变量是引用类型则不能再指向其他对象
例如:final ArrayList中可以修改添加值,只是引用不能指向其他arraylist
super 关键字的作用
(1)访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
(2)访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
(3)this 和 super 不能同时出现在一个构造函数里面,因为 this 必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
访问权限有哪些
在修饰成员变量/成员方法时,该成员的四种访问权限的含义如下:
- private:该成员可以被该类内部成员访问;
- default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问;
- protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问;
- public:该成员可以被任意包下,任意类的成员进行访问。
在修饰类时,该类只有两种访问权限,对应的访问权限的含义如下:
- default:该类可以被同一包下其他的类访问;
- public:该类可以被任意包下,任意的类所访问。
i++和++i的区别是什么
自增运算符(++)
自减运算符(- - )
当运算符放在变量之前时(前缀),先自增/减,再赋值;
当运算符放在变量之后时(后缀),先赋值,再自增/减。
移位运算符
移位运算符是最基本的运算符之一,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
在 Java 代码里使用 <<
、 >>
和>>>
转换成的指令码运行起来会更高效些。
Java 中有三种移位运算符:
-
<<
:左移运算符,向左移若干位,高位丢弃,低位补零。 -
>>
:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。 -
>>>
:无符号右移,忽略符号位,空位都以 0 补齐。
由于 double
,float
在二进制中的表现比较特殊,不能来进行移位操作。
移位操作符实际上支持的类型只有int
和long
,编译器在对short
、byte
、char
类型进行移位前,都会将其转换为int
类型再操作。
如果移位的位数超过数值所占有的位数会怎样?
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。
也就是说:x<<42
等同于x<<10
,x>>42
等同于x>>10
,x >>>42
等同于x >>> 10
。
continue、break 和 return 的区别
-
continue
:指跳出当前的这一次循环,继续下一次循环。 -
break
:指跳出整个循环体,继续执行循环下面的语句。 return
用于跳出所在方法,结束该方法的运行。
switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
在 switch(expr 1) 中,expr1 只能是一个整数表达式或者枚举常量。
而整数表达式可以是 int 基本数据类型或者 Integer 包装类型。由于,byte、short、char 都可以隐式转换为 int,所以,这些类型以及这些类型的包装类型也都是可以的。
而 long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式的转换为 int 类型,所以,它们不能作用于 switch 语句中。
不过,需要注意的是在 JDK1.7 版本之后 switch 就可以作用在 String 上了。
& 和 && 的区别是什么
Java 中 && 和 & 都是表示与的逻辑运算符,都表示逻辑运算符 and
&&:有短路功能,当第一个表达式的值为 false 的时候,则不再计算第二个表达式;
&:不管第一个表达式结果是否为 true,第二个都会执行。
& 还可以用作位运算符:当 & 两边的表达式不是 Boolean 类型的时候,& 表示按位操作。
基本数据类型
Java 中的几种基本数据类型了解么?
整数型:
- byte:1字节(8位)默认值:0
- short:2字节(16位)默认值:0
- int:4字节(32位)默认值:0
- long:8字节(64位)默认值:0L
浮点型: - float:4字节(32位)默认值:0f
- double:8字节(64位)0d
字符型: - char:2字节(16位)默认值:’u0000’
布尔型: - boolean:1位 默认值:false
基本类型与包装类的区别
Java语言是面向对象的语言,8种基本数据类型不具备对象的特性。所以Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
- 用途:包装类型可用于泛型,基本类型不可以。
- 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。 - 占用空间:相比于包装类型, 基本数据类型占用的空间往往非常小。
- 默认值:成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。 - 比较方式:对于基本数据类型来说,
==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有包装类对象之间值的比较,全部使用equals()
方法
包装类型的缓存机制了解么
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制。
下面的代码的输出结果是 true
还是 false
呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//false
Integer i1=40
这一行代码会发生装箱,这行代码等价于 Integer i1=Integer.valueOf(40)
。因此,i1
直接使用的是缓存中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象。
自动装箱、自动拆箱是什么,原理是什么
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
自动装箱时,编译器调用valueOf将原始类型值转换成对象,
自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法,将对象转换成原始类型值。
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
浮点数运算的时候为什么会有精度丢失的风险
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
这个和计算机保存浮点数的机制有很大关系。
计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
如何解决浮点数运算的精度丢失问题
BigDecimal
类 可以实现对浮点数的运算,不会造成精度丢失。
通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal
来做的。
超过 long 整型的数据应该如何表示
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。
BigInteger
内部使用 int[]
数组来存储任意大小的整形数据。相对于常规整数类型的运算来说,BigInteger
运算的效率会相对较低。
变量
成员变量和局部变量的区别
Java中的变量分为成员变量和局部变量
- 语法形式:成员变量是在类的范围里定义的变量;局部变量是在方法里定义的变量;
- 默认值:成员变量有默认初始值;( 0, 0.0, ‘\u0000’, false );局部变量没有默认初始值;
- 存储方式:未被static修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,被static修饰的成员变量也叫类变量,它存储于方法区中;局部变量存储于栈内存中
- 生命周期:未被static修饰的成员变量是对象的一部分,生命周期与对象相同,被static修饰的成员变量是类的一部分,生命周期与类相同;局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
静态变量有什么作用
静态变量也就是被 static
关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。
静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
通常情况下,静态变量会被 final
关键字修饰成为常量
字符型常量和字符串常量的区别
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。
方法
静态方法为什么不能调用非静态成员
需要结合 JVM 的相关知识,主要原因如下:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法有什么不同
1、调用方式
调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
重载和重写有什么区别
重载:同样的一个方法能够根据输入数据的不同,做出不同的处理
重写发生在编译器,在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写:当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
为什么不能重写 private 或者 static 方法
static:因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。
private:因为 private 修饰的变量和方法只能在当前类中使用, 如果是其他的类继承当前类是不能访问到 private 变量或方法的。
什么是可变长参数
可变长参数就是允许在调用方法时传入不定长度的参数。
public static void method1(String... args) {
//......
}
可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
Java 的可变参数编译后实际会被转换成一个数组
遇到方法重载时会优先匹配固定参数的方法
Java 中只有值传递
Java 中将实参传递给方法(或函数)的方式是 值传递 :
- 参数是基本类型:传递的是基本类型的字面量值的拷贝,会创建副本。
- 参数是引用类型:传递的是实参所引用的对象在堆中地址值的拷贝,也会创建副本。
在 Java 中定义无参内容为空的构造方法有什么作用
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“无参构造方法”。
如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。