基础语法
1、Java 中的8种基本数据类型
Java 中有 8 种基本数据类型,分别为:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 1 种字符类型:
char
- 1 种布尔型:
boolean
这 8 种基本数据类型的默认值以及所占空间:
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768 ~ 32767 |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char | 16 | 2 | 'u0000' | 0 ~ 65535 |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | 4 or 1 | false | true、false |
如果boolean是单独使用:boolean占4个字节。
如果boolean是以“boolean数组”的形式使用:boolean占1个字节。
提示:以上结论是《Java虚拟机规范》一书给的,也就是说,仅仅适用于那些遵循了“规范”的JVM,换句话说,如果某个JVM实现没有遵循规范,boolean到底几个字节就又无法得知了。
这八种基本类型对应的包装类:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
。
包装类型没有默认值、不赋值就是 Null
,而基本类型有默认值。
另外,这个问题建议还可以先从 JVM 层面来分析:
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们都知道对象类型的实例都存在于堆中。
《深入理解 Java 虚拟机》 :局部变量表主要存放了编译期可知的基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
2、包装类型的常量池技术
Java 基本类型的包装类型(Float、Double除外)都实现了常量池技术。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Character
缓存源码:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
Boolean
缓存源码:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
两种浮点数类型的包装类 Float
, Double
并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
下面我们来看一下问题。下面的代码的输出结果是 true
还是 false
呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
Integer i1=40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40)
。因此,i1
直接使用的是常量池中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象!(有new关键字就是创建新对象!)
因此,答案是 false
。你答对了吗?
3、自动装箱、拆箱
什么是自动拆、装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型
Integer i = 10; //装箱
int n = i; //拆箱
上面这两行代码对应的字节码为:
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 intValue()
方法。
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
注意:如果频繁拆装箱的话,也会严重影响系统的性能,我们应该尽量避免不必要的拆、装箱操作!
private static long sum() {
// 应该使用 long 而不是 Long
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
4、switch case 语句中的变量类型?
Java5 以前,case 只能是 byte、short、char、int
从 Java 5 开始,Java 中引入了枚举类型, case 也可以是 enum 类型
从 Java 7 开始,case还可以是 String 类型
5、访问修饰符public、private、protected、以及不写(默认)时的区别?
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
6、break ,continue ,return 的区别及作用?
break 跳出总上一层循环,不再执行循环(结束当前的循环体)
continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
演示代码:
public class MyTest { public static void main(String[] args) { testBreak(); testContinue(); testReturn(); } /** * break测试方法 */ private static void testBreak() { for (int i = 0; i < 5; i++) { if (i == 3) { break; } System.out.println("--break------------" + i); } } /** * continue测试方法 */ private static void testContinue() { for (int i = 0; i < 5; i++) { if (i == 3) { continue; } System.out.println("--continue------------" + i); } } /** * Return测试方法 */ private static void testReturn() { for (int i = 0; i < 5; i++) { if (i == 3) { return; } System.out.println("--return------------" + i); } } }
输出:
--break------------2 --continue------------0 --continue------------1 --continue------------2 --continue------------4 --return------------0 --return------------1 --return------------2
7、重载、重写的区别?
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理,并且发生在编译期。
重写
重写是子类对父类中允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,子类方法返回值类型、抛出的异常应是父类方法返回值类型子类或者与父类相同,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
,则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回值类型 | 可修改 | 是父类方法返回值类型子类或者与父类相同 |
异常 | 可修改 | 是父类方法返回值类型子类或者与父类相同 |
访问修饰符 | 可修改 | 访问修饰符范围大于等于父类 |
发生阶段 | 编译期 | 运行期 |
⭐️ 关于 重写的返回值类型 额外补充:如果方法的返回类型是 void 和基本数据类型,则返回值类型重写时不可修改;如果方法的返回值是引用类型,重写时是可以返回相同的引用类型或该引用类型的子类。
8、可变长参数(Java语法糖)
所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 method1
方法就可以接受 0 个或者多个参数:
public static void method1(String... args) {
//......
}
另外,可变参数只能作为函数的最后一个参数,但其前面可以有,也可以没有任何其他参数:
public static void method2(String arg1, String... args) {
//......
}
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
我们通过下面这个例子来证明一下。
public class VariableLengthArgument {
public static void printVariable(String... args) {
for (String s : args) {
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
public static void main(String[] args) {
printVariable("a", "b");
printVariable("a", "b", "c");
}
}
输出:
ab
a
b
c
另外,Java 的可变参数编译后实际会被转换成一个数组,反编译 上述源码编译后 生成的 class
文件就可以看出来:
public class VariableLengthArgument {
public static void printVariable(String... args) {
String[] var1 = args;
int var2 = args.length;
for(int var3 = 0; var3 < var2; ++var3) {
String s = var1[var3];
System.out.println(s);
}
}
public static void printVariable(String arg1, String arg2) {
System.out.println(arg1 + arg2);
}
}
9、final、finally、finalize的区别?
final 用于修饰变量、方法和类。
- final 变量:被修饰的变量不可变,不可变分为
引用不可变
和对象不可变
,final 指的是引用不可变
,final 修饰的变量必须初始化,通常称被修饰的变量为常量
。 - final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
- final 类:被修饰的类不能被继承,其内部所有方法不能被重写。
finally 作为异常处理的一部分,它只能在 try/catch
语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0)
可以阻断 finally 执行。
finalize 是在 java.lang.Object
里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc
启动后、对象被回收时被调用。
一个对象的 finalize 方法只会被调用一次,但是finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐在代码中使用 finalize 方法。
10、为什么要有static关键字?
通常,当我们用new来创建对象时,对象的数据存储空间才被分配,其方法才能被外界调用。
但是有两种情形,用上述方法是无法解决的。
一种情形是,你只想为某特定数据分配一份存储空间,而不用考虑创建任何对象就能直接访问(常见的比如,创建缓存常量键值对的Map)
另一种情形 是,即使没有创建对象,也能够调用类中的某个方法
此时,通过static关键字,可以满足这两方面的需求!
11、是否可以覆盖(override)一个static方法?
static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用!
12、静态方法中为啥不能调用非静态成员变量/方法?
主要原因:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问;而非静态成员变量/方法是属于实例对象的,只有在实例化对象之后才存在,只能通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
13、静态方法里面能不能引用静态资源?
可以,因为都是类初始化的时候加载的,大家相互都认识的!
14、非静态方法里面能不能引用静态资源?
可以,非静态方法就是实例方法,是在new之后才产生的;而静态资源时在类new之前就已经存在!
15、静态变量、代码块、静态方法的执行顺序?
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
1、普通代码块执行顺序:静态代码块—> 初始化块 —> 构造函数—> 普通代码块
演示代码:
public class Initializationblock {
int a;
int b;
public Initializationblock() {
System.out.println("构造函数");
}
{
a = 10;
b = 15;
System.out.println("初始化块");
}
static {
System.out.println("静态初始化块");
}
public void method(){
{
System.out.println("普通初始化块");
}
}
}
public class Demo1 {
public static void main(String[] args) {
Initializationblock initializationblock = new Initializationblock();
initializationblock.method();
}
}
输出:
静态初始化块
构造初始化块
构造函数
普通初始化块
2、继承关系中的代码块执行顺序:父类静态块—>子类静态块—>父类初始化块—>父类构造函数—>子类初始化块—>子类构造函数
演示代码:
public class BaseOne {
public BaseOne() {
System.out.println("BaseOne构造器");
}
{
System.out.println("BaseOne初始化块");
}
static {
System.out.println("BaseOne静态初始化块");
}
}
public class BaseTwo extends BaseOne {
public BaseTwo() {
System.out.println("BaseTwo构造器");
}
{
System.out.println("BaseTwo初始化块");
}
static {
System.out.println("BaseTwo静态初始化块");
}
}
public class Demo2 {
public static void main(String[] args) {
BaseTwo baseTwo = new BaseTwo();
}
}
输出:
BaseOne静态初始化块
BaseTwo静态初始化块
BaseOne初始化块
BaseOne构造器
BaseTwo初始化块
BaseTwo构造器
16、面向对象和面向过程的区别?
面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展。
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低。
17、面向对象三大特征:封装、继承、多态
封装是指把一个对象的属性信息隐藏在对象内部,不允许在对象外部直接访问对象内部的属性信息,但是可以提供一些可以被外界访问的方法来操作属性。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性!
public class Student {
private int id;//id属性私有化
private String name;//name属性私有化
//获取id的方法
public int getId() {
return id;
}
//设置id的方法
public void setId(int id) {
this.id = id;
}
//获取name的方法
public String getName() {
return name;
}
//设置name的方法
public void setName(String name) {
this.name = name;
}
}
继承的好处,子类拥有父类的所有属性和方法,从而实现了实现代码的高度复用。
特点:
子类拥有父类对象所有属性和方法(包括私有属性和私有方法),但是父类中的私有属性和私有方法子类无法访问,只是拥有
子类可以拥有自己属性和方法,即子类可以对父类进行扩展
子类可以重写(Override)父类的方法
顾名思义,表示一个对象可以具有多种状态,具体表现为父类的引用指向子类的实例,也可表述为同一事件发生在不同的对象实例上会产生不同的结果。
特点:
对象类型和引用类型之间具有 继承(类)or 实现(接口) 的关系
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
多态不能调用“只在子类存在但在父类 or 接口中不存在的方法
如果子类重写了父类的方法,真正执行的是子类覆盖的方法;如果子类没有覆盖父类的方法,则执行的是父类的方法
18、如何实现多态?
本质上多态分两种:
1、编译时多态(又称静态多态)
2、运行时多态(又称动态多态)
方法重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行的时候调用的是确定的方法!
我们通常所说的多态都是指运行时多态,也就是编译时不能确定究竟调用哪个具体方法,一直延迟到运行时才能确定!
Java实现多态有 3 个必要条件:继承、重写和向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
- 向上转型:在多态中需要将子类的引用赋值给父类变量,只有这样该引用才既能可调用父类的方法,又可调用子类的方法
19、重载(Overload)和重写(Override)的区别是什么?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重写发生在子类与父类之间,重写方法返回值和形参都不能改变,与方法返回值和访问修饰符无关,即重写的方法不能根据返回类型进行区分。即外壳不变,核心重写!
- 重载是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(最常用的地方就是构造器的重载)都必须有一个独一无二的参数类型列表!
20、接口和抽象类有什么共同点和区别?
共同点 :
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(Java 8 可以用
default
关键在接口中定义默认方法)
区别 :
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为;抽象类主要用于代码复用,即抽出公共代码以便子类直接调用
- 一个类只能继承一个类,但可以实现多个接口
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值;而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值 - 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象
- 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计
21、抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类!
22、什么是不可变对象?
用final 修饰的类实例化后的对象都是不可变对象!不可变对象指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象,如 String、Integer及其它包装类。不可变对象最大的好处是线程安全!
能否创建一个包含可变对象的不可变对象?
当然可以,比如final Person[] persons = new Persion[]{}
, persons
是不可变对象的引用,但其数组中的Person实例却是可变的!这种情况下需要特别谨慎,不要轻易共享不可变对象的引用,这种情况下,如果数据需要变化时,最好的方式是返回原对象的一个拷贝!
23、所有对象的祖先:Object
Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
1. public final native Class<?> getClass()
native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
2. public native int hashCode()
native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
3. public boolean equals(Object obj)
用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
4. protected native Object clone() throws CloneNotSupportedException
naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,
表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。
Object本身没有实现Cloneable接口,
所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
5. public String toString()
返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
6. public final native void notify()
native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
如果有多个线程在等待只会任意唤醒一个。
7. public final native void notifyAll()
native方法,并且不能重写。
跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
8. public final native void wait(long timeout) throws InterruptedException
native方法,并且不能重写。暂停线程的执行。
注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
9. public final void wait(long timeout, int nanos) throws InterruptedException
多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。
所以超时的时间还需要加上nanos毫秒。
10. public final void wait() throws InterruptedException
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
11. protected void finalize() throws Throwable { }
实例被垃圾回收器回收的时候触发的操作