java基础不牢固容易踩的坑
经过一年java后端代码以及对jdk源码阅读之后的总结,对java中一些基础中的容易忽略的东西写下来,给偏爱技术热爱开源的Coder们分享一下,避免在写代码中误入雷区。
(注:如无特殊说明,均以jdk8为基础,本文所有例子均已通过编译器通过,且对输出进行了验证)。
1.关于基本类型的包装类的。
基本类型boolean、char、byte、short、int、long、float、double。是java的特殊类型,特殊性在于区别于对象的存储,对象存储的是引用,引用指向在jvm堆中分配的值,基本类型直接存储的就是值,能提高效率。
同时java遵循面向对象思想为每个基本类型都提供了封装类:Boolean、Character、Byte、Short、Integer、Float、Double。
坑1:变量赋值与类型转换。
变量赋值其实并不算是个坑,因为编译器会自动检查,例如long var = 2;编译器会报错。
类型转换分为自动转换和强制转换,这里不在赘述,具体转换规则自行查询。
赋值的时候 = 和+=的区别,+=会自动转换类型。
short num;
num = num + 1; //error
num += 1; //ok
坑2:计算
整数相除默认只保留整数,即使赋值给浮点类型也不行。
double d = 5 / 2;
System.out.println(d); //2
byte相加超出长度后数值会变得很怪异。
byte num = 127;
num += 1;
System.out.println(num);//-128
两个float相加结果会存在一定的误差等等。
float a1 = 1.001f;
float a2= 1.819f;
float a3 = a1 + a2;
System.out.println(a3);//jdk8: 2.8200002
System.out.println(12.0 - 11.9 == 0.1) //false
坑3:装箱与拆箱
int a =100;
Integer b = 100;
Integer c= 100;
System.out.println(a==b); //true
System.out.println(b==a); //true
System.out.println(b==c); //true
基本类型和包装类型运算时会自动拆箱,所以ab相等;
当把100换成200 b==c会返回false。因为==比较的是引用;
b==c为true的原因是Integer采用了缓存,对-128到127之间的数据不再自动生成,而是直接引用(请看Integer中的IntegerCache内部类),类似于String;
2. null值
1.null关键字,大小写敏感
2.null是引用类型的默认值
4.null既不是对象也不是类型,可以强制转换成任何引用类型。
String s = (String) null; //ok
int a = (int) null; //error
4.null值的引用类型变量,instance会返回false,如下:
Integer iAmNull = null;
System.out.println(iAmNull instanceof Integer); //false
5.null值的引用变量调用非静态方法,会抛npe,调用静态方法是可以的。
3 void,Void
void在逻辑上是一种数据类型,但不是基本类型,也不是引用类型。我们暂且不管它到底是什么类型,因为很多人都说不清。
void提供了包装类Void,看源码我们会发现它被定义成final,而且构造方法是private,也就是说不能实例化。
Void类型只能赋值为null,而void不能赋值,仅仅用来作为方法返回值输出。
Void能作为方法输入参数当做占位符,只能传值为null。
4.多态
1.父类引用能指向子类对象,调用的方法具体取决于引用的对象,而不是取决于引用。
public class A {
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
public String show(B obj)...{
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{}
class D extends B{}
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b)); ①
System.out.println(a1.show(c)); ②
System.out.println(a1.show(d)); ③
System.out.println(a2.show(b)); ④
System.out.println(a2.show(c)); ⑤
System.out.println(a2.show(d)); ⑥
System.out.println(b.show(b)); ⑦
System.out.println(b.show(c)); ⑧
System.out.println(b.show(d)); ⑨
结果
① A and A
② A and A
③ A and D
④ B and A
⑤ B and A
⑥ A and D
⑦ B and B
⑧ B and B
⑨ A and D
2.子类对父类方法不可见的情况下是不会覆盖的,而是重新定义了一个方法。
3.继承关系只有方法会覆盖,成员变量不会被覆盖。
public class A {
protected int i = 1;
public void show(){
System.out.println(i);
}
}
public class B extends A{
private int i = 10;
public void show(){
System.out.println(i);
}
}
A a = new A();
A a1 = new B();
B b = new B();
a.show(); //1
a1.show(); //10
b.show(); //10
5. super
super并没有代表超类的一个引用的能力,只是代表调用父类的方法而已。
public class Test extends Number{
public static void main(String[] args) {
new Test().test();
}
private void test(){
System.out.println(super.getClass().getName()); //获取父类方法名getClass().getSuperClass().getName();
}
}
这里应结合多态的override来理解上面的输出。
6.字符串
老生常谈的问题了,字符串采用常量池缓存,不宜创建太多字符串,subString、new、+、等操作慎用,会创建很多字符串常量无法回收,当运行久了之后会占用越多越多的内存。
字符串做参数,并不会改变改变实参的值。
7.多线程
线程安全的问题建议单独去看。充分考虑到线程安全问题,不会出现死锁问题。
Object提供的wait、notify、notify不建议对多线程了解不深入的人去用。
建议使用可重入锁替代synchronized。
多线程知识较多,这里不做详细说明。
8.异常处理
1.异常处理块中可以继续抛异常。
2.try块可以不需要catch或finally,但二者必须至少有一个
3.finially块中return 语句会覆盖try块中的return,finally块在try块代码执行完后,return语句之前执行。
public class MyClass {
public static void main(String[] args) {
System.out.print(new MyClass().getNum()); //4
}
int getNum(){
try {
System.out.println("try block");
return 3;
} finally {
System.out.println("finally block");
return 4;
}
}
}
输出结果是:
try block
finally block
4.碰到事务方法,异常处理要特别注意。
5.finially不一定必执行,当在try块中有system.exit(1);
try {
System.out.println("try block begin");
System.exit(1);
System.out.println("try block end");
} catch (Exception e){
System.out.println("catch block begin");
System.exit(1);
System.out.println("catch block end");
} finally {
System.out.println("finally block");
}
以上代码输出try block begin
6.异常不建议往上抛,特殊情况除外。
9.正则表达式
replace、split等所有以正则表达式作为参数的方法
一定要注意正则表达式的含义,例如如下输入
String a = "acb..";
System.out.println(a.replaceAll(".","b")); //bbbbb
转义字符串,尤其是路径问题
10.静态相关
static可以修饰类(包含内部类)、成员方法、成员变量、类中代码块。
static只能修饰类变量,不能修饰局部变量,编译器会报错。
类启动加载顺序。静态>非静态,成员变量>代码块>构造方法,父类>子类。
静态方法和静态变量会随类的加载而加载,静态内部类只有在使用时才会加载。
static 不能和abstract同时使用,可以和final同时使用。
11 循环删除
在list中删除a,看起来一切正常。如下所示。
List<String> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
for (String str : list){
if ("b".equals(str)){
list.remove(str);
}
}
System.out.println(list); //[a, c]
假如删除a呢?ConcurrentModificationException,自己模拟下流程思考下原因。
12. 特殊关键字
1.volatile:一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
2.transient:修饰成员变量。当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化。
3.strictfp:一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的,符合IEEE-754规范的。
例如一个类被声明为strictfp,那么该类中所有的方法都是strictfp的。
4.native:原生态方法,可以调用其他语言。这里不做详细说明。
13 枚举
1.枚举为每个枚举对象创建一个实例,在首次使用时初始化。
public static void main(String[] args) {
weekday mon = weekday.mon;
}
public enum weekday {
mon, tue, wes, thus, fri;
private weekday() {
System.out.println("hello"); //输出hello五次
}
}
2.构造方法可以传值。
public static void main(String[] args) {
weekday mon = weekday.mon;
}
public enum weekday {
mon, tue(1), wes(2), thus, fri;
private weekday() {
System.out.print("hello ");
}
private weekday(int a) {
System.out.print("ok ");
}
}
输出:hello ok ok hello hello
14 泛型
1. 泛型类、泛型方法,用<>表示,<>内的内容只要符合变量命名规范即可,不要求是T、K、E、V
2. 泛型可以有多个变量,例如public Class MyClass<T1,T2,T3,T4>,一般1到2个。
3. Set<Integer> 不是Set<Number>的子类,逻辑上不具备任何继承关系,二者都属于Set类。Set<Integer>赋值给Set<Number>会报错。
4. 上面一行的解决方式是泛型通配符。
5. 泛型的类型参数只能是引用类型,如Set<int>编译报错。
6. 不能对确切的泛型类型使用instance操作,
7. <T extends Number>作用于方法或者类上,而 <? extends Number> 则不可以。
8. 泛型运行期即被擦除,所以不能通过Type type = new TypeToken<TestGeneric<String>>(){}.getType(); 这种方式在运行期动态获取泛型类型。
9. 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。