java基础不牢固容易踩的坑

时间:2022-11-06 19:16:54

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. 泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。