0%

java易错点

在 JAVA 语言中容易被忽略的问题

摘自于Leetcode java突击面试宝典,仅供个人学习参考,转载请注明出处!

1. 基本的数据结构

  1. 直接量 概念:

    直接量是在程序中直接出现的常量值

    整数类型的直接量 默认是 int 类型

    浮点类型的直接量 默认是double 类型

  2. 整数类型的直接量 (默认是 $int$ 类型) 赋值 给整数类型的变量

    将整数类型的直接量赋值给整数类型的变量时,只要直接量没有超出变量的取值范围,即可直接赋值;

    如果直接量超出了变量的取值范围,则会导致编译错误。

  3. 如果整数类型的直接量 超过了$int$ 的取值范围,就必须在直接量后面加上 l ,将变量显性声明为 long 类型,否则会导致编译错误

  4. 如果要将直接量表示成 float 类型,则必须在其后面加上字母 F 或 f。将 double 类型的直接量赋值给 float 类型的变量是不允许的,会导致编译错误。

2. 面向对象

  1. 静态,Java 的类成员(成员变量、方法等)可以是静态的或实例的。使用关键字 static 修饰的类成员是静态的类成员,不使用关键字 static 修饰的类成员则是实例的类成员。

    static修饰的方法,只能访问静态的方法/变量 ,不能访问实例的类成员

    而 实例方法 既可以访问 实例的类成员,也可以访问静态的类成员

3. 重载的条件

  1. 函数名必须相同
  2. 参数列表必须不同(个数不同,类型不同,参数类型的排列顺序不同)
  3. 返回类型 可以相同 可以不同
  4. 仅 返回类型不同 不能构成方法的重载
  5. 重载是发生在编译时,因为编译器可以根据参数的类型类选择使用哪种方法

4. 重写

​ 重载 与 重写 名字相似但 两者是完全不同的关系,

  1. 方法的重写 是描述子类与父类之间的,而重载指的的在一个类中
  2. 重写的方法必须要和父类保持一致,包括返回值类型,方法名,参数列表

5. 类中初始化的顺序

  1. 静态属性 初始化
  2. 静态方法块初始化
  3. 普通属性 初始化
  4. 普通方法块初始化
  5. 构造函数 初始化
  6. 普通方法
  7. 例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MyClass
{
//静态属性
private static String staticField = getStaticField();
//静态方法块
static {
System.out.println(staticField);
System.out.println("静态方法块初始化");
}
//普通属性
private String field=getField();

//普通方法块
{
System.out.println("普通方法块初始化");
}
//构造方法

public MyClass()
{
System.out.println("构造函数初始化");
}

private String getField()
{
String filed = "普通属性初始化";
return filed;
}

private static String getStaticField()
{
String statiFiled = "静态属性初始化";
return statiFiled;
}

public static void main(String[] args)
{
MyClass myClass=new MyClass();
}

//打印结果
静态属性初始化
静态方法块初始化
普通方法块初始化
构造函数初始化
}

6. Static

  1. static 修饰 成员变量 称为 类变量

    可以直接通过 类名.类变量名 访问或修改 不需要通过对象访问

  2. static 修饰 成员函数 称为 静态方法

    • 可以直接通过 类名.方法名 调用 ,不需要通过对象访问
    • 静态方法 只能访问静态的成员变量 或者静态的方法
    • 静态方法中 不能含有 this 关键字
  3. static 修饰 内部类(static只能修饰内部类)了解

    一般内部类的定义都是如下形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Outter
    {
    public static String name = "Outter";

    public class Inner //只有public 修饰符
    {
    public void test()
    {
    System.out.println("Inner为测试而生!");
    }
    }
    }

    这种内部类的实例化方法 是:

    1
    Outter.Inner inner=new Outter().new Inner();

    而被static 修饰了的内部类 是如下形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Outter
    {
    public static String name = "Outter";

    public static class StaticInner //内部类被 static修饰
    {
    public void test()
    {
    System.out.println("StaticInner为测试而生!");
    }
    }
    }

    static修饰的实例化方法是:

    1
    Outter.StaticInner staticInner=new Outter.StaticInner();

7. Final

  1. final 修饰 类

    表示该类不可被继承,并且该类的成员方法 也会被隐式的指定为final,成员变量 根据需要 自己设置是否被final 修饰

  2. final 修饰 方法

    表示这个方法不能被子类重写

  3. final 修饰 变量

    • 修饰基本数据类型 表示该值不可被修改
    • 修饰引用数据类型 表示其对其初始化以后便不能再能其指向另一个对象

8. 接口

  1. 包含的方法
    • 抽象方法(只有函数的声明)
    • 默认方法
    • 静态方法
    • 私有方法
  2. 接口只能使用 public default 修饰,default是缺省值,表示具有包内访问权限控制
  3. 接口不能被实例化
  4. 接口的实现类 必须将接口中的抽象方法重写 否则 该类为抽象类

9.函数式接口

视频教程:狂神说

函数式接口(Function interface) 是java8新引入的这一种。

  1. 定义:任何一个接口,如果接口中只包含唯一一个抽象方法,那么就是函数式接口

  2. 特性:对于函数式接口,可以采用Lambda表达式来创建该接口的对象

  3. 为什么要引入lambda 表达式,还是为了简化代码,梳理一下 要调用一个接口的实现类 改进过程

    • 在同一个包下,创建Main class ,接口,以及 接口的实现类 三个文件
    • 在同一个.java文件下 ,在Main.class {}外部 创建 一个实现类 和接口
    • 在同一个.java文件下,在Main.class{}内部 pswm 外部 创建一个静态外部类,接口还是在Main.class{}外部
    • 在同一个.java文件下,在Main.class{}内部 pswm 内部 创建一个局部内部类,接口还是在Main.class{}外部
    • 在同一个.java文件下,在Main.class{}内部 pswm 内部 创建一个匿名内部类,接口同上
    • 使用Lambda表达式(代码如下)
  4. 示例:下面两个线程都是打印一句话

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class LambdaPractice {

    public static void main(String[] args) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("匿名内部类实现");
    }
    }).start();

    new Thread(() -> System.out.println("lambda表达式实现")).start();
    }
    }

10. 抽象类

抽象类 是一种抽象能力弱于接口的类

特征:

  1. 包含抽象方法的类 一定是抽象类
  2. 抽象类不一定包含 抽象方法
  3. 在抽象类中 还可以定义 成员变量 成员函数 构造方法 静态变量 静态方法
  4. 抽象类不能被实例化

11. 多线程编程

  1. thread中.start() 与 .run()方法的区别?

    image-20210509160506254

    • .start():用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

    • .run(): run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public class ThreadTest {
      public static void main(String[] args) {
      Thread t = new Thread() {
      @Override
      public void run() {
      pong();
      }

      };
      t.run();
      System.out.println("主线程");
      }

      static void pong() {
      System.out.println("子线程");
      }
      }
      打印结果:
      子线程
      主线程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      public class ThreadTest {
      public static void main(String[] args) {
      Thread t = new Thread() {
      @Override
      public void run() {
      pong();
      }

      };
      t.start();
      System.out.println("主线程");
      }

      static void pong() {
      System.out.println("子线程");
      }
      }

      打印结果:
      主线程
      子线程
  2. .sleep()与.wait()的区别?

    • 相同点:一旦执行方法,都可以使得当前的线程进入等待状态。

    • 不同点:

      1. 函数声明的位置不同,sleep是在Thread类中,wait是在Object类中

      2. 关于是否可以指定睡眠时间,sleep函数必须指定,wait可以指定也可以不指定

      3. slee() 会让当前正在运行的、用CPU时间片的线程挂起指定时间,休眠时间到自动苏醒进入可运行状态(阻塞->就绪->执行)

        wait() 方法用来线程间通信,如果设置了时间,就等待指定时间;如果不设置,则该对象在其它线程被调用 notif()/ notifyAll()方法后进入可运行状态,才有机会竞争获取对象锁。

      4. 适用场景不同,sleep方法可以在任何需要的场景下调用,而wait方法必须在同步代码块中或者 同步方法中的监视器中调用

      5. 关于是否释放同步监视器,如果两方法都是使用在同步代码块或同步方法中,sleep()不会释放锁,wait会释放锁,并进入线程等待池。

      6. sleep线程控制自身流程。wait用来线程间通信,使拥有该对象锁的线程等待直到指定时间或 notify

    • 代码

      1. 执行sleep()方法 不会释放锁

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        public class ThreadSleepWait implements Runnable {
        int number = 10;

        public void firstMethod() throws Exception {
        synchronized (this) {
        number += 100;
        System.out.println(number);
        }
        }

        public void secondMethod() throws Exception {
        synchronized (this) {
        /**
        * (休息2S,阻塞线程)
        * 以验证当前线程对象的机锁被占用时,
        * 是否被可以访问其他同步代码块
        */
        Thread.sleep(2000);
        number *= 200;
        System.out.println(number);
        }
        }

        @Override
        public void run() {
        try {
        firstMethod();
        } catch (Exception e) {
        e.printStackTrace();
        }
        }

        public static void main(String[] args) throws Exception {
        ThreadSleepWait threadSleepWait = new ThreadSleepWait();
        Thread thread = new Thread(threadSleepWait);
        thread.start();
        threadSleepWait.secondMethod();
        }
        }
        打印结果:
        2000//先执行了secondMethod
        2100
      2. 执行wait()方法释放索

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        public class ThreadSleepWait implements Runnable {
        int number = 10;

        public void firstMethod() throws Exception {
        synchronized (this) {
        number += 100;
        System.out.println(number);
        }
        }

        public void secondMethod() throws Exception {
        synchronized (this) {
        /**
        * (休息2S,阻塞线程)
        * 以验证当前线程对象的机锁被占用时,
        * 是否被可以访问其他同步代码块
        */
        this.wait(2000);
        number *= 200;
        System.out.println(number);
        }
        }

        @Override
        public void run() {
        try {
        firstMethod();
        } catch (Exception e) {
        e.printStackTrace();
        }
        }

        public static void main(String[] args) throws Exception {
        ThreadSleepWait threadSleepWait = new ThreadSleepWait();
        Thread thread = new Thread(threadSleepWait);
        thread.start();
        threadSleepWait.secondMethod();
        }
        }
        执行结果:
        110//先执行secondMethod 但由于wait方法释放了锁 就会导致子线程得到资源 可以执行
        22000

12. == 与 equals()

  1. ==是运算符、.equals()是方法(来自于父类Object)
  2. 使用==比较两个对象的内存地址是否相同。使用.equals()是比较两个对象的内容是否相同,比较值
  3. 如果没有重写.equals(),仍然比较的是地址

更加具体而言:

  • 通过==比较基本数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static void main(String[] args) 
    {
    // integer-type
    System.out.println(10 == 20);

    // char-type
    System.out.println('a' == 'b');

    // char and double type
    System.out.println('a' == 97.0);

    // boolean type
    System.out.println(true == true);
    }

    输出结果:
    false
    false
    true
    true
  • 通过==比较自定义数据类型

    前提 需要保证两个对象的类型是相同的 或者 是父类子类之间的关系,否则会产生编译错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) 
    {
    Thread t = new Thread();
    Object o = new Object();
    String s = new String("GEEKS");

    System.out.println(t == o);
    System.out.println(o == s);

    // Uncomment to see error
    System.out.println(t==s);
    }
    输出结果:
    false
    false
    //error
  • equals()

    equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

    1
    2
    3
    public boolean quals(Object obj) {
    return (this == obj);
    }
  • 特殊情况 Stirng s=”abc” 与 String s=new String(“abc”)

    String s=”abce”是一种非常特殊的形式,和new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。以String s=”abce”;形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为”abcd”的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个”abcd”,下一次如果有String s1 = “abcd”;又会将s1指向”abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.而String s = new String(“abcd”);和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。

      也可以这么理解: String str = “hello”; 先在内存中找是不是有”hello”这个对象,如果有,就让str指向那个”hello”.如果内存里没有”hello”,就创建一个新的对象保存”hello”. String str=new String (“hello”) 就是不管内存里是不是已经有”hello”这个对象,都新建一个对象保存”hello”。

13. 集合

image-20210510214432749

image-20210510220226119

  1. ArrayList

    • 实现了List接口的可扩容数组(动态数组),基于数组实现

    • 线程不安全容器,代替使用:

      1
      List list = Collections.synchronizedList(new ArrayList(...))
    • ArrayList扩容后的数组长度会增加50%(查看源码得知)

  2. Vector

    • Vector同 ArrayList一样,都是基于数组实现的,只不过 Vector是一个线程安全的容器,它对内部的每个方法都简单粗暴的上锁,避免多线程引起的安全性问题,但是通常这种同步方式需要的开销比较大,因此,访问元素的效率要远远低于 ArrayList
    • 还有一点在于扩容上, ArrayList扩容后的数组长度会增加50%,而 Vector的扩容长度后数组会增加一倍。
  3. LinkedList

    • 双向链表实现,允许存储null

    • 不安全的容器,必须加锁 或者 使用

      1
      List list = Collections.synchronizedList(new LinkedList(...))
  4. Stack

    • LIFO 后进先出

    • 继承自Vector 所以是线程安全的容器

    • 构造方法

      1
      Deque<Integer> stack = new ArrayDeque<Integer>()
  5. HashMap

    • HashSet是一个利用哈希表原理来存储元素的集合,允许存放null

    • 非线程安全,HashTable是线程安全的容器

    • 性能影响因素:初始容量以及加载因子

    • 根据阿里巴巴Java开发手册上建议HashMap初始化时设置已知的大小,如果不超过16个,那么设置成默认大小16:

      initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为0.75, 如果暂时无法确定初始值大小,请设置为16(即默认值)。

14. 泛型

  1. 泛型类

    一个普通的类:

    1
    2
    3
    class Father {

    }

    泛型类:

    1
    2
    3
    4
    5
    6
    7
    class Father<E> {
    E sex;

    public void add(E a) {
    System.out.println(a);
    }
    }

    若实例化一个泛型类对象时,没有指定泛型的类型,会默认为Object类型

    1
    2
    3
    4
    5
    //1.泛型类不指定类型时 默认是Object
    Father father = new Father();
    father.add("abc");
    father.add(1);
    father.add(1.2);

    若实例化一个泛型类对象时,指定了泛型的类型,那么 操作的数据必须时该类型

    1
    2
    3
    4
    //2.泛型类指定类型时, 类中的E 就都是该类型
    Father<Integer> father1 = new Father<>();
    father1.add(1);
    // father1.add("2");报错
  2. 继承自一个泛型类的情况

    一个子类继承自一个泛型类父类

    若指定了父类中 泛型的类型,子类就不需要是泛型类了

    1
    2
    3
    4
    //父类指定泛型
    class Son2 extends Father<Integer> {

    }
    1
    2
    3
    4
    //3.父类指定泛型类型,继承的子类就不需要指定泛型类型了 可以直接使用
    Son2 son2 = new Son2();
    son2.add(1);
    // son2.add("1");//报错

    若没有指定父类中的泛型的类型,子类就需要变成泛型类

    1
    2
    3
    4
    5
    6
    7
    8
    9
            //5. 父类不指定泛型类 子类指定泛型类
    Son<Integer> son1 = new Son<>();
    son1.add(1);
    // son1.add(1.0);//报错
    //父类不指定泛型类 子类也不指定泛型类,那么E就默认是Object
    Son son3 = new Son();
    son3.add(1);
    son3.add(1.0);
    son3.add("1");
  3. 细节

    1. <>不一定只有一个泛型,可以有多个

    2. 泛型类的构造器的写法,不带泛型类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Father<E> {
      E sex;

      //构造器错误写法
      public Father<E>(){

      }

      //构造器正确写法
      public Father(){

      }

      public void add(E a) {
      System.out.println(a);
      }
      }
    3. 不同的泛型类型 但是是同一个集合类 ,不可以相互赋值

      1
      2
      3
      List<Integer> list = new ArrayList<>();
      List<String> list2 = new ArrayList<>();
      // list = list2;//报错
    4. 泛型类中的静态方法 不允许有泛型参数

      这是因为静态方法是随着类的加载而被加载,先于对象存在,而泛型的参数是实例化对象时才去指定,也就是加载顺序不同

    5. 不能直接使用E[] 来创建数组

      1
      2
      //        E[] arr = new E[10];//错误
      E[] arr = (E[]) new Object[10];
  4. 泛型方法

    • 什么是泛型方法?

      并不是形参当中带了 泛型的参数就是泛型方法了 ,要求这个方法的泛型的参数类型要与当前类的泛型参数无关

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class TestGeneric02<E> {
      //这并不是泛型方法
      public void a(E m) {

      }

      //这才是泛型方法,T的确定是在调用这个方法的时候 才确定下来
      public <T> void b(T n) {

      }
      }
    • 泛型方法对应的那个泛型参数类型 和 当前 所在的这个类是否是泛型类,泛型是啥 无关,下面这个类不是泛型类 但仍然可以包含泛型方法

      1
      2
      3
      4
      5
      6
      public class TestGeneric02 {

      public <T> void b(T n) {

      }
      }
    • 泛型方法可以是静态方法,泛型类中的带了泛型类型的方法不能是静态方法

      这是因为泛型方法中 泛型参数与所在类是否是泛型类 泛型的参数无关。

      虽然静态方法优先于对象的加载,但是泛型方法的泛型类型是在调用时才被确定的

  5. 泛型参数中的继承关系

    多态的一种体现:父类指针 指向子类对象,如下面代码块1

    但是 在下面的代码块2 String 虽然是Object类的子类 但是 List 与 List 并没有继承关系,List 与 List 是同级关系 底层数据的存储 都是Object数组

    1
    2
    3
    4
    5
    6
    7
    Object[] objects = new Object[10];
    Object[] objects1 = new Object[10];
    String[] strings = new String[10];
    String[] strings1 = new String[10];
    objects = strings;//多态的一种体现 父类-》子类
    objects = objects1;
    strings = strings1;
    1
    2
    3
    4
    List<String> stringList = new ArrayList<>();
    List<Object> objectList = new ArrayList<>();

    // objectList = stringList;//报错
  6. 泛型受限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
            /*泛型的上限 定义了上限 
    List<? extends Person> 是 List<Person> 的父类 以及 List<Person子类> 的父类
    如果想要使用 父类 = 子类对象 子类类型不能超过 Person
    */

    List<? extends Person> list = null;
    // list = objectList;报错
    list = personList;//正确 父类 ->子类
    list = studentList;//正确 父类 ->子类

    /*泛型的下限
    List<? super Person> 是 List<Person> 的父类 以及List<Person 父类> 的 父类
    */

    List<? super Person> list1 = null;
    list1 = objectList;//正确 父类 ->子类
    list1 = personList;//正确 父类 ->子类
    // list1 = studentList;//报错
  7. 泛型通配符

    List<?> list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Test02 {
    /*
    以下 前三个方法冲突 List<Object> List<String> List<Integer> 是同级关系 相当于一个方法了
    */
    public void a(List<Object> list) {

    }

    public void a(List<String> list) {

    }

    public void a(List<Integer> list) {

    }

    public void a(List<?> list){
    for(Object o:list){
    System.out.println(o);
    }
    }
    }