基本语法
在Java中,标识符和关键字的区别是什么?
在编写程序的过程中,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说标识符就是一个名字
而有一些标识符,Java语言为其赋予了特殊的含义,只能用于特定的地方,这些特殊的标识符就是关键字,关键字就是被赋予特殊含义的标识符
Java语言的关键字
| 分类 | 关键字 | ||||||
|---|---|---|---|---|---|---|---|
| 访问控制 | private | protected | public | ||||
| 类、方法和变量修饰符 | abstract | class | extends | final | implements | interface | native |
| new | static | strictfp | synchronized | transient | volatile | enum | |
| 程序控制 | break | continue | return | do | while | if | else |
| for | instanceof | switch | case | default | assert | ||
| 错误处理 | try | catch | throw | throws | finally | ||
| 包相关 | import | package | |||||
| 基本类型 | boolean | byte | char | double | float | int | long |
| short | |||||||
| 变量引用 | super | this | void | ||||
| 保留字 | goto | const |
default这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制
- 在程序控制中,当在
switch中匹配不到任何情况时,可以使用default来编写默认匹配的情况- 在类,方法和变量修饰符中,从JDK8开始引入了默认方法,可以使用
default关键字来定义一个方法的默认实现- 在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符
default,但是这个修饰符加上了就会报错
基本数据类型
Java中的几种基本数据类型?
在Java中有8中基本数据类型,分别为:
- 6种数字类型
- 4种整数型:byte、short、int、long
- 2种浮点型:float、double
- 1种字符类型:char
- 1种布尔型:boolean
String不算基本数据类型,它只代表一个类,属于引用类型,String默认值也是null
8种基本数据类型
| 基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
|---|---|---|---|---|
| byte | 8 | 1 | 0 | -128~127 |
| short | 16 | 2 | 0 | -32768(-2^15)~ 32767(2^15-1) |
| int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
| long | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
| char | 16 | 2 | ‘u0000’ | 0 ~ 65535(2^16 - 1) |
| float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
| double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
| boolean | 1 | false | true、false |
为什么像
byte、short、int、long能表示的最大正数都减1了?因为在二进制补码表示法中,最高位是用来表示符号的(0表示正数,1表示负数),其余位数表示数值部分,所以如果我们要表示最大的正数,就需要把除了最高位之外的所有位都设为1,如果我们再加1,就会导致溢出,变成一个负数
对于
boolean,官方文档并未明确定义,其依赖于JVM厂商的具体实现,逻辑上的理解是占用1位,但是实际中会考虑计算机高效存储因素,另外Java的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化,这种所占存储空间大小的不变性是Java程序比其他大多数语言编写的程序更具可移植性的原因之一
Considerations
- Java里使用
long类型的数据一定要在数值后面加上L,否则将作为整型解析char a = 'h':单引号,String a = "hello":双引号
基本类型和包装类型的区别?
- 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量,并且,包装类型可以用于泛型,而基本类型不可以
- 存储方式:
- 基本数据类型的局部变量都存放在Java虚拟机栈中的局部变量表中
- 基本数据类型的成员变量(未被
static修饰)存放在Java虚拟机的堆中 - 包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中
- 占用空间:相比于包装类型(对象类型),基本数据类型占用的空间往往非常小
- 默认值:成员变量包装类不赋值就是
null,基本类型有默认值且不是null - 比较方式
- 对于基本数据类型来说,
==比较的是值 - 对于包装数据类型来说,
==比较的是对象的内存地址 - 所有整型包装类之间值得比较,全部使用
equals()方法
- 对于基本数据类型来说,
为什么说几乎所有对象实例都存在于堆中呢?
因为HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,从而避免堆上分配内存
HotSpot:JVM规范的一种实现,除了HotSpot之外还有J9 VM、Zing VM、JRockit VM等JVM
JIT(Just in Time Compilation)编译器:属于运行时编译,当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用
Considerations
基本数据类型存放在栈中是一个很常见的误区!基本数据类型的存储位置取决于它们的作用域和声明方式,如果它们是局部变量,那么就会存放在栈中,如果它们是成员变量,那么就会存放在堆中
public class Test { // 成员变量,存放在堆中 int a = 10; // 被 static 修饰,也存放在堆中,但属于类,不属于对象 // JDK1.7 静态变量从永久代移动了 Java 堆中 static int b = 20; public void method() { // 局部变量,存放在栈中 int c = 30; static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量 } }
包装类型的缓存机制?
Java基本数据类型的包装类型的大部分都用到了缓存机制来提升性能
Byte、Short、Integer、Long这四种包装类默认创建了数组[-128, 127]的相应类型的缓存数据Integer缓存源码private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } /** * Returns an {@code Integer} instance representing the specified * {@code int} value. If a new {@code Integer} instance is not * required, this method should generally be used in preference to * the constructor {@link #Integer(int)}, as this method is likely * to yield significantly better space and time performance by * caching frequently requested values. * * This method will always cache values in the range -128 to 127, * inclusive, and may cache other values outside of this range. * * @param i an {@code int} value. * @return an {@code Integer} instance representing {@code i}. * @since 1.5 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
Character创建了数值在[0, 127]范围的缓存数据Character缓存源码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); } } /** * Returns a <tt>Character</tt> instance representing the specified * <tt>char</tt> value. * If a new <tt>Character</tt> instance is not required, this method * should generally be used in preference to the constructor * {@link #Character(char)}, as this method is likely to yield * significantly better space and time performance by caching * frequently requested values. * * This method will always cache values in the range {@code * '\u005Cu0000'} to {@code '\u005Cu007F'}, inclusive, and may * cache other values outside of this range. * * @param c a char value. * @return a <tt>Character</tt> instance representing <tt>c</tt>. * @since 1.5 */ public static Character valueOf(char c) { if (c <= 127) { // must cache return CharacterCache.cache[(int)c]; } return new Character(c); }
Boolean直接返回TRUEorFALSEBoolean缓存源码/** * Returns a {@code Boolean} instance representing the specified * {@code boolean} value. If the specified {@code boolean} value * is {@code true}, this method returns {@code Boolean.TRUE}; * if it is {@code false}, this method returns {@code Boolean.FALSE}. * If a new {@code Boolean} instance is not required, this method * should generally be used in preference to the constructor * {@link #Boolean(boolean)}, as this method is likely to yield * significantly better space and time performance. * * @param b a boolean value. * @return a {@code Boolean} instance representing {@code b}. * @since 1.4 */ public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡
两种浮点型的包装类
Float、Double并没有实现缓存机制
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1 == i2);
上述代码中,第一行会发生自动装箱,等价于Integer i1 = Integer.valueOf(40);,因此i1这个变量直接使用的是缓存中的对象,而Integer i2 = new Integer(40);会直接创建新的对象,因此输出的结果为false
所有整型包装类对象之间值的比较,全部使用equals()方法进行比较
说明:对于Integer var = ?在[-128, 127]之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,在此区间内的Integer值可以直接使用
==进行判断,但是在这个区间外的所有数据,都会在堆上产生,并不会复用已有对象,此乃大坑,推荐使用equals()方法进行判断
自动装箱与自动拆箱?原理?
什么是自动拆装箱?
- 装箱:将基本数据类型用他们对应的引用类型包装起来
- 拆箱:将包装类型转换为基本数据类型
原理:
- 自动装箱调用了包装类的
valueOf()方法Integer i = 10等价于Integer i = Integer.valueOf(10)
- 自动拆箱调用了包装类
xxxValue()方法int n = i等价于int n = i.intValue();
为什么浮点型运算的时候会有精度丢失的风险?
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来做的
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
超过long整型的数据应该如何表示?
基本数据类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险
在Java中,64位long整型是最大的整数类型
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true
BigInteger内部使用int[]数组来存储任意大小的整型数据
相对于常规整数类型的运算来说,BigInteger运算的效率会相对较低
变量
成员变量和局部变量的区别?
- 语法形式
- 成员变量是属于类的,局部变量是在代码块或者方法中定义的变量或是方法的参数
- 成员变量可以被
public、private、static等修饰符所修饰,而局部变量不能被访问控制修饰符以及static修饰 - 成员变量和局部变量都可以被
final修饰
- 存储方式
- 如果成员变量被
static修饰,则该成员变量是属于类的,如果没有被static修饰,则该成员变量是属于实例的 - 对象存在于堆内存,局部变量存在于栈内存
- 如果成员变量被
- 生存时间
- 成员变量是对象的一部分,它随着对象的创建而存在,
- 局部变量随着方法的调用而自动生成,随着方法调用的结束而消亡
- 默认值
- 成员变量如果没有被赋予初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final修饰的成员变量也必须显式赋值) - 局部变量不会自动赋值
- 成员变量如果没有被赋予初始值,则会自动以类型的默认值而赋值(一种情况例外:被
成员变量&局部变量代码演示
public class VariableExample {
// 成员变量
private String name;
private int age;
// 在方法中的局部变量
public void method() {
// 在栈中分配的局部变量
int number1 = 10;
// 在栈中分配的局部变量
String str = "Hello";
System.out.println(str);
}
// 有形参的方法中的局部变量
public void method(int number) {
// 在栈中分配的局部变量
int sum = number + 2;
System.out.println(sum);
}
// 构造器中的局部变量
public VariableExample(String name, int age) {
// 对成员变量进行赋值
this.name = name;
// 对成员变量进行赋值
this.age = age;
// 在栈中分配的局部变量
int number = 20;
// 在栈中分配的局部变量
String str = "Hello, " + this.name + ".!";
System.out.println(number);
System.out.println(str);
}
}
为什么成员变量有默认值?
- 如果没有默认值,变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外
- 默认值设置有两种方式:手动和自动,根据第一点,没有手动赋值就一定要自动赋值,成员变量在运行时可以借助反射等方法手动赋值,而局部变量不行
- 对于编译器(javac)来说,局部变量有没有赋值很好判断,可以直接报错,而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采取自动赋默认值
静态变量有什么作用?
静态变量就是被static关键字修饰的变量,其被类的所有实例共享,无论一个类创建了多少对象,他们都共享同一份静态变量,即:静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存
静态变量通过类名来访问,例如:StaticVariableExample.staticVar
public class StaticVariableExample {
// 静态变量
public static int staticVar = 0;
}
但是如果变量被private关键字修饰就无法这样访问了
通常情况下,静态变量会被final关键字修饰成常量
public class ConstantVariableExample {
// 常量
public static final int constantVar = 0;
}
字符型常量和字符串常量的区别?
- 形式
- 字符常量是单引号引起的一个字符
- 字符串常量是双引号引起的0个或若干个字符
- 含义
- 字符常量相当于一个整型值(ASCII码),可以参加表达式运算
- 字符串常量代表一个地址值(该字符串在内存中存放的位置)
- 占内存大小
- 字符常量:2个字节
- 字符串常量:若干个字节
public class StringExample {
// 字符型常量
public static final char LETTER_A = 'A';
// 字符串常量
public static final String GREETING_MESSAGE = "Hello, world!";
}
方法
静态方法为什么不能调用非静态成员?
和JVM有关,原因:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问
- 在类的非静态成员不存在的时候静态方法已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
public class Example {
// 定义一个字符型常量
public static final char LETTER_A = 'A';
// 定义一个字符串常量
public static final String GREETING_MESSAGE = "Hello, world!";
public static void main(String[] args) {
// 输出字符型常量的值
System.out.println("字符型常量的值为:" + LETTER_A);
// 输出字符串常量的值
System.out.println("字符串常量的值为:" + GREETING_MESSAGE);
}
}
静态方法和实例方法有什么不同?
- 调用方式
- 在外部调用静态方法时,可以使用
类名.方法名的方式,也可以使用对象.方法名的方式,而实例方法只有后者这一方式,即:调用静态方法可以无需创建对象 - 一般不建议使用
对象.方法名的方式来调用静态方法,这种方式非常容易造成混淆,静态方法不属于类的某个对象,而是属于这个类,因此一般建议使用前者来进行调用
- 在外部调用静态方法时,可以使用
- 访问类成员是否存在限制
- 静态方法在访问本类的成员时,只允许访问静态成员(静态成员变量和静态方法),不允许访问实例成员(实例成员变量和实例方法)
- 实例方法不存在上述限制
重写和重载有什么区别?
重载:方法名相同,形参不同,方法可以根据输入的形参不同做出不同的处理
重写:当子类继承父类时,形参一致,但是要在方法内做出别的操作时,需要通过重写来覆盖父类方法
| 区别点 | 重载方法 | 重写方法 |
|---|---|---|
| 发生范围 | 同一类 | 子类 |
| 形参列表 | 必须修改 | 一定不能修改 |
| 返回类型 | 可修改 | 子类方法返回值类型比父类方法的返回值类型更小或相等 |
| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类抛出的异常类更小或相等 |
| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
| 发生阶段 | 编译期 | 运行期 |
可变形参?
允许在调用方法时传入不定长度的形参
public static void method1(String... args) {
// ......
}
可变形参只能作为形参列表的最后一个形参,可变形参的前面可以有也可以没有任何形参
public static void method2(String arg1, String... args) {
// ......
}
遇到方法重载时怎么办?会优先匹配固定形参的方法还是可变形参的方法?
答:优先匹配固定形参的方法
此外,可变形参在编译后实际会被转换成一个数组
