Java知识点整理

时间:2021-10-23 08:29:18
  1. 基本数据类型

    Java内置8种基本类型 -> 6种数字类型 -> 4种整数类型: byte, short, int, long

    ​ -> 2种浮点类型: float, double

    ​ -> 1种布尔类型: boolean

    ​ -> 1种字符类型: char

    (类型大小写注意,比如boolean是原始数据类型,Boolean是对应的封装对象)

    • 数据类型特征表
    类型 位数 最小值 最大值 默认值 其他
    byte 8 -128(-2^7) 127(2^7-1) 0 有符号、二进制补码表示
    short 16 -32768(-2^15) 32767(2^15-1) 0 有符号、二进制补码表示
    int 32 -2^31 2^31-1 0 有符号、二进制补码表示
    long 64 -2^63 2^63-1 0L(0l) 有符号、二进制补码表示
    float 32 2^(-149) 2^128-1 0.0f 单精度、IEEE754标准
    double 64 2^(-1074) 2^1024-1 0.0d 双精度、IEEE754标准
    char 16 \u0000(0) \uffff(65535) \u0000(0) 单一的、Unicode字符
    • 浮点数内存结构
    类型 位数 符号位 指数位 尾数位
    float 32 1 8 23
    double 64 1 11 52
    • 原始数据类型对应的封装对象

      • (byte, Byte), (short, Short), (long, Long), (float,Float), (double, Double), (boolean, Boolean)
      • (int, Integer), (char, Character)
    • 小题

      Integer i = null;
      int j = i.intValue();

      编译通过,但运行时报错NullPointerException。因为调用了null.somemethod()。

    • 自动装箱和拆箱

      Integer i = 100;  //自动装箱,编译器执行Integer.valueOf(100)
      int j = i; //自动拆箱,编译器执行i.intValue()
    • 小题

      Integer i1 =200;  
      Integer i2 =200;
      System.out.println("i1==i2: "+(i1==i2));
      Integer i3 =100;
      Integer i4 =100;
      System.out.println("i3==i4: "+(i3==i4));

      运行结果为false,true.

      首先,==和equals()的区别:

      • ==比较的是两个对象的引用是否相同,或者是比较原始数据类型是否相等;
      • equals()比较的是两个对象的内容是否相同。

      其次,-128~127的Integer值可以从缓存中取得。其他情况要重新创建。

      public static Integer valueOf(int i) {
      if (i >= IntegerCache.low && i <= IntegerCache.high)
      return IntegerCache.cache[i + (-IntegerCache.low)];
      return new Integer(i);
      }
    • 小题

      String str1 ="abc";
      String str2 ="abc";
      System.out.println(str2==str1);
      System.out.println(str2.equals(str1));
      String str3 =new String("abc");
      String str4 =new String("abc");
      System.out.println(str3==str4);
      System.out.println(str3.equals(str4));

      结果是true,true, false, true.前两个String对象从String池中获取,后两个对象是新创建的,内容相同但引用不同。

    • 小题

      String d ="2"; 
      String e ="23";
      e = e.substring(0, 1);
      System.out.println(e.equals(d));
      System.out.println(e==d);

      结果是true,false.直接上源码:

      public String substring(int beginIndex, int endIndex) {
      if (beginIndex < 0) {
      throw new StringIndexOutOfBoundsException(beginIndex);
      }
      if (endIndex > value.length) {
      throw new StringIndexOutOfBoundsException(endIndex);
      }
      int subLen = endIndex - beginIndex;
      if (subLen < 0) {
      throw new StringIndexOutOfBoundsException(subLen);
      }
      return ((beginIndex == 0) && (endIndex == value.length)) ? this
      : new String(value, beginIndex, subLen);
      }
      • 关于基本数据类型的几点补充

        1. short s1=1; s1=s1+1;

        这一句编译错误,因为执行s1+1返回的结果是int类型(执行隐式类型转换)。修改的话要强制转换为short型才可以。

        short s1=1; s1+=4;

        这一句没有任何问题。

        1. switch语句不能作用于long类型中,可以作用于char, byte, short, int, Character, Byte, Short, Integer, String or an enum.
      • 小题
      public static void main(String[] args) throws Throwable {
      int j=0;
      for(int i=0;i<1000;i++) {
      j=j++;
      }
      System.out.println(j);
      }

      运行结果为0

      解释一(未十分确信):Java使用中间缓存变量机制,j=j++语句会执行为:

      temp=j;
      j=j+1;
      j=temp;

      解释二(靠谱):使用javap反汇编命令进行反汇编,其中j=j++对应的结果是(j对应的变量编号是1):

      11: iload_1 //将局部变量j的值放到栈顶:0
      12: iinc 1, 1 //将局部变量j的值加1,j=1
      15: istore_1 //将栈顶的值放到局部变量j中,j=0
      所以从底层实现看,j=j++这一句中的自增操作只是对局部变量的操作,局部变量变化后没有存储到栈顶,反而被之前栈顶的值覆盖了,所以相当于不起作用。

  2. 关键字

    • final:
      • 修饰变量时,用以定义常量;
      • 修饰方法时,方法不能被重写(Override);
      • 修饰类时,类不能被继承。
  3. 数组

    • 3种创建方式

      int[] arr1 = {1,2,3,4};             //正确
      int[] arr2 = new int[4]; //正确
      int[] arr3 = new int[]{1,2,3,4}; //正确
      int[] arr4 = new int[4]{1,2,3,4};s //错误,编译不通过
    • 数组越界,抛出ArrayIndexOutOfBoundsException

    • 数组具有length属性

    • 如果不对数组指定初值,默认初始化为相应数据类型的默认值。

    • 多维数组,嵌套中括号即可。

  4. String

    • 不属于基本类型

    • 创建

      String s1="ss";                 //先将"ss"存储在池中,然后返回引用
      String s2=new String("ss"); //创建新对象,使用的"ss"已经在池中
      String s3="Prog"+"gramming"; //创建3个对象,均存储在池中
    • 字符串常量池和不可变(Immutable)字符串

      字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。原文

    • 小题

      String str1 = "str";
      String str2 = "ing";
      String str3 = "str" + "ing";
      String str4 = str1 + str2;
      System.out.println(str3 == str4);
      String str5 = "string";
      System.out.println(str3 == str5);

      结果是false,true。后一个结果应该很好理解,因为第3行代码执行时已经将”string”存储在常量池中,第6行代码返回的是常量池中同一字符串”string”的引用,所以结果为true。前一个结果可能有点疑惑,后来把这段代码编译反汇编后发现执行第3行代码时是直接生成的String对象,内容为”string”;而执行第四行代码时是借助new StringBuilder().append(“str”).append(“ing”).toString(),关键在于StringBuilder中定义的toString()方法,返回的是一个重新创建的String对象,并没有存在池中,所以前一个结果为false。

      //StringBuilder.toString()源代码
      public String toString() {
      // Create a copy, don't share the array
      return new String(value, 0, count);
      }
    • 基本数据类型封装类的parse方法可能会报NumberFormatException,比如Integer.parseInt("era");

    • intern()方法:若由equals()判断池中包含相同的对象则返回池中该对象的引用,否则将新对象加入池中并返回引用。注意不管是怎么创建的,都先从池里找

    • 对于字符串拼接,考虑性能,如果循环进行拼接操作,生成的字符串都会存在池里,并且每次拼接都要重新构造StringBuilder对象,影响性能。因此可以使用StringBuilder优化:

      StringBuilder builder...append(String.valueOf("fsdasf"));
    • StringBuilder/StringBuffer

      • StringBuilder线程不安全,但效率高;
      • StringBuffer线程安全,但效率低。

      线程安全:某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成

  5. 集合

    • 类型:List, Set, Map,都是接口而非具体实现。

    • List

      • 特点是有序并且按线性方式存储,可以根据元素的下标进行精准的控制。

      • ArrayList是顺序表的体现,底层用数组实现。

      数组在线性表中的分类属于顺序表,即:顺序表中的数据元素存储是连续的,内存划分的区域也是连续的。

      • LinkedList是链表的体现,使用双向链表实现。(链表的三种形式:单向链表、双向链表、循环链表)

      链表在物理存储上通常是非连续、非顺序的方式存储的,但是数据元素的逻辑顺序是连续的,实现方式是通过链表中的引用来实现。

      • Stack是Vector的子类,而Vector实现了List<E>接口;是栈结构的代表。

      栈和队列是特殊的线性表,或者说是受到限制的线性表,其限制是仅允许在线性表的尾部进行添加和删除操作,这一端被称为栈顶,另一端称为栈底。

    • Queue

      • 队列Queue直接继承自Collection接口,是队列结构的代表,使用链表结构实现。

      Queue接口是队列的体现,在实现上是基于链表实现的,但是具体的实现类是LinkindList,也就是说,java通过Queue接口收窄了LinkedList的访问权限,只提供从队尾,队头等的操作,从而实现了对列。

      (注:Queue继承自Collection接口,而LinkedList实现了Deque接口,Deque接口继承自Queue接口,即实现了Queue接口的所有方法

      • Queue接口的主要方法:add, offer(添加元素), poll(返回并删除队列头部元素)。

      根据offer()方法的官方注解,更加推荐使用offer()方法:

      /** 
      * Inserts the specified element into this queue if it is possible to do
      * so immediately without violating capacity restrictions.
      * When using a capacity-restricted queue, this method is generally
      * preferable to {@link #add}, which can fail to insert an element only
      * by throwing an exception.
      * ...
      * @return {@code true} if the element was added to this queue, else
      * {@code false}
      * ...
      **/
    • Set

      • 直接继承自Collection接口,HashSet内部使用HashMap实现;
      • 特点是无序但是不包含重复的元素;
      • 元素存放方式为散列存放,即通过计算hash值存放元素。
    • Map

      • Map单独为一个接口,HashMap是基于哈希表的对Map接口的实现,而哈希表的底层数据结构是数组和链表;
      • 特点是能够根据key快速查找value;键必须唯一,put一个键相同的值时该键会被更新,即同一个键只能映射到一个值。
      • 键值对以Entry类型的对象实例存在(注:Entry是Map接口中定义的内部接口)
  6. 面向对象

    • 三大特征:封装、继承和多态

    • 构造函数:创建对象时调用。若未显式定义构造函数,系统自动生成无参构造函数。

    • 重载:发生在一个类中,函数名相同,参数列表不同(类型/个数)

    • 重写:发生在两个类中,函数名相同,参数列表相同。

    • 继承:初始化子类时先初始化父类,即调用构造函数时隐式执行父类构造函数。

    • 单继承性:Java允许一个类只能有一个父类。

    • super关键字:既可以作为父类对象的引用调用父类方法,也可以作为父类构造函数名显式调用父类构造函数。

    • 垃圾回收:对象被回收时会调用finalize()方法。

      垃圾回收机制:当垃圾回收器(Garbage Collector)认定对象没有任何引用时会将其回收,在回收前调用finalize方法。但是《Java编程思想》中还提到了这样一句话:

      记住,无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。

      看起来Java的垃圾清理机制并不是那么完美。调用System.gc()方法可以强制进行垃圾回收。

      附一篇讲Java垃圾回收机制比较好的文章

    • 多态:

      Employee e=new Manager();

      假定Employee类没有定义abc(),而Manager类定义abc(),如果调用e.abc()则编译不通过。

    • 封装:

      访问修饰权限:private, default, public, protected

    修饰符 当前类 同一包内 子孙类 其他包 其他包子孙类
    public Y Y Y Y Y
    protected Y Y Y N Y/N(说明
    default Y Y N N N
    private Y N N N N

    protected关键字特别说明:

    • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
    • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法
  7. 抽象类和接口

    • 抽象类不能被实例化,实例化由子类完成;但是抽象类也是类,可以有构造方法!

    • 抽象类不一定要有抽象方法;抽象方法所在的类一定是抽象类;

    • abstract和final不能同时修饰一个类,否则编译不通过。因为final声明的类不能被继承,如果同时修饰意味着这个类永远是抽象类。

    • abstract不能与private, static(?), final, native(?)的同时修饰一个方法,否则编译不通过。

    • 模板方法模式

      抽象类体现的就是一种模板设计模式,抽象类作为多个子类通用的模板,子类在抽象类的基础上进行扩展,这种模式解决的就是一部分功能不确定,就把不确定的功能部分暴露出去,让子类自己去实现。

    • 接口是更高级别的抽象,接口中的方法必须都是抽象方法;接口中声明的方法都是没有方法主体的;

    • 接口中的属性默认修饰符public static final,方法默认修饰符public abstract,这些都可以省略。

    • 一个类可以实现多个接口,但只能继承一个父类。

    • 接口和抽象类的主要区别

      • 抽象类是一种对事物的抽象,而接口是一种对行为的抽象;

      • 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

      • 抽象类是一种模板式设计,而接口是一种行为规范,是一种辐射式设计。

      模板式设计的含义是:如果B和C都使用了公共模板A,如果他们的公共部分需要改动,那么只改动A就可以了;

      辐射式设计的含义是:如果B和C都实现了公共接口A,如果现在要向A中添加新的方法,那么B和C都必须进行相应改动。

  8. 异常处理

    Java知识点整理

    • Error和Exception, checked异常和unchecked异常

      • Error(unchecked异常)

      是程序无法处理的错误,通常发生于虚拟机自身,表示运行应用程序中较严重问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

      • Exception(checked异常)

      必须处理的异常:Checked异常是Java特有的,在java设计哲学中,Checked异常被认为是可以被处理或者修复的异常,所以Java程序必须显式处理Checked异常,当我们使用或者出现Checked异常类的时候,程序中要么显式try- catch捕获该异常并修复,要么显式声明抛出该异常,否则程序无法通过编译。

      (注:以上内容引用他人文章,原作者声明:作者:胖先森链接:https://juejin.im/post/5a81941ff265da4e976e9004来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。)

    • try-catch-finally

      可以用多个catch子句处理不同异常,级别由低到高。

    • 在Java SE 7或者更高版本中,一个catch块可以同时处理多种异常类型,有助于减少重复代码。

      //...
      } catch (ClassNotFoundException | SQLException e) {
      e.printStackTrace();
      }

      注意当catch语句要捕获多种异常类型时,参数(在这里是e)隐式地成为final变量。

    • 方法声明异常throws,抛出异常throw。

    • try-with-resources子句(Java SE 7以后)

      援引官网的介绍吧:

      The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

  9. 线程

    • 线程是操作系统调度的最小单元,也叫轻量级进程。同一进程可以创建多个线程,而他们都拥有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

    • 线程的5个状态

      • 创建(New):使用new关键字创建一个线程
      • 就绪(Ready):调用start方法,等待CPU调度
      • 运行(Running):执行run方法
      • 阻塞(Blocked):由于一些原因放弃CPU使用权,暂时停止执行
      • 死亡(Dead):run方法执行完毕或者执行时产生异常
    • 几个重要的名词区分

      • 同步和异步

      同步方法调用后必须等待方法返回才能执行后续行为;异步方法调用后可以立刻执行后续行为。

      • 并发和并行

      并行是真正意义上的多个任务同时执行;并发是支持处理多个任务,不一定要同时,多个任务可能是串行的,但每个任务只能获取CPU很短的占用时间,多个任务在很短的时间内交替执行。

      我相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

      • 阻塞和非阻塞

      阻塞是指某一线程访问一公共资源时其他线程必须等待该线程释放资源才可以使用,否则就要挂起线程等待;非阻塞是指线程之间不会发生资源争夺。

      • 原子性

      原子性是指一个操作是不可被中断的,即使多个线程是同时执行的。

      • 可见性

      可见性是指当某个线程修改了共享变量的值,其他线程能否立刻知晓。

      • 有序性

      Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

      Java中提供了关键字volatile和synchronized关键字来保证线程之间操作的有序性。volatile包含了禁止指令重排序的语义,并保证不同线程对同一变量操作时的可见性;synchronized关键字对同一时刻同一变量只允许一个线程对其进行lock操作。

    • 线程创建

      • 继承Thread类重写run()方法
      • 匿名内部类,即在new Thread()后跟具体的定义体,其中重写了run()方法
      • 实现Runnable接口,重写run()方法
    • 开启线程的run()和start()方法区别

      • run()方法不能新建一个线程,而是在当前线程中调用run()方法;
      • start()方法新建一个线程并调用其run()方法。
    • 终止线程不要使用stop()

      一般情况下,线程在执行完毕后就会结束,无需手工关闭,但是我们也经常会创建无限循环的后台进程以持续提供某项服务,所以就需要手动关闭这些线程。
      在JDK中也有终止线程的API,例如stop()方法,但是极度不推荐这个方法,因为stop()方法得到调用后,会强行把执行到一半的线程终止,可能会引起数据不一致问题。

      但是想要终止一个无限循环的线程应该怎么做?

      我们推荐的做法是在类中添加一个isStop的布尔值属性,判断isStop为true则跳出循环体,线程执行完毕自动终止,就避免了数据不一致的问题。

    • wait() 和notify()

      这两个方法不是Thread类特有的,而是所有类的父类Object中的。

      当一个对象调用了wait方法后,如:objectA.wait(),当前线程就会在这个对象上等待,会释放该对象的锁,直到其他线程调用了objectA.notify()方法为止。
      需要注意的是,wait和notify方法都必须获得对象的监视器(锁),在同步代码得到执行后也会释放对象的锁,所以必须被包含在对象的synchronzied语句中。

      (注:以上内容援引他人文章,原作者声明:作者:kk_miles链接:https://juejin.im/post/5a30a7466fb9a0450f21ec5c来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。)

    • 小题

      public class WaitNotifyDemo {
      final static Object object = new Object();
      public static class Thread1 extends Thread{
      @Override
      public void run() {
      synchronized (object){
      System.out.println(System.currentTimeMillis()+" 线程1开启。。");
      try {
      object.wait(); //1.先获取object的锁,然后开始等待,并再次释放object的锁。
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.println(System.currentTimeMillis()+" 线程1结束。。"); //4f. 两秒后,线程2执行结束,线程结束,重新获得了object的锁,此句猜得到执行
      }
      }
      }
      public static class Thread2 extends Thread{
      @Override
      public void run() {
      synchronized (object){
      System.out.println(System.currentTimeMillis()+" 线程2开启,并执行notify通知线程1 。。");
      object.notify(); //2.获取object的锁成功,通知object的其他线程(即线程1),这里还未释放object的锁!
      System.out.println(System.currentTimeMillis()+" 线程2执行notify结束。。");
      try {
      Thread.sleep(2000); //3. 使线程2暂停2秒,即2秒后线程2才能执行结束,才能把object的锁释放。
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.println(System.currentTimeMillis()+" 线程2结束。。");
      }
      }
      }

      public static void main(String[] args) {
      Thread thread1 = new Thread1();
      Thread thread2 = new Thread2();
      thread1.start();
      thread2.start();
      }
      }

      以上代码是一个关于wait和notify的示意代码,声明一个静态对象和两个静态内部类表示两个线程,连个线程分别在对象object的同步块中调用wait和notify方法,在线程1中调用了object的wait方法前需要先获取object的锁,然后进入线程等待,并释放锁;然后线程2的notify方法执行前需要获取object的锁,然后通知线程1。但是此时线程1仍然无法执行wait方法后面的代码,原因是线程2Thread.sleep(2000)使得线程2在2秒之后才能退出并且释放object的锁,也即线程1必须等待线程2的object同步代码块执行结束后才能获得锁,去继续执行下面的代码,具体的输出日志如下:

      1510300350884 线程1开启。。
      1510300350884 线程2开启,并执行notify通知线程1 。。
      1510300350884 线程2执行notify结束。。
      1510300352884 线程2结束。。 //2秒中之后线程2结束,释放object锁
      1510300352885 线程1结束。。 //线程2释放锁之后,线程1获得锁,结束等待状态,继续向下执行。
    • 生产者消费者问题(线程同步,互斥)

  10. I/O操作

    • 流是一组有顺序的, 有起点和终点的字节集合,是对数据传输的总称或抽象。

    • I/O流的分类

      • 根据处理数据类型划分为:字符流和字节流
      • 根据数据流向划分为:输入流和输出流
    • 字符流和字节流

      字符流本质上就是基于字节流,读取时查询相应的码表。

      区别:

      • 读写单位:字节流以字节为单位;字符流以字符为单位,一次可能读多个字节;

      • 处理对象:字节流能处理所有类型的数据(图片、视频等),而字符流只能处理字符类型的数据。

      (文本数据优先考虑字符流,其他情况都用字节流)

    • Java 基本I/O操作参考我之前的文章

  11. 网络编程

    • Java建立TCP连接的步骤:
      1. 服务器实例化一个ServerSocket对象,通过服务器的特定端口通信;
      2. 服务器调用ServerSocket的accept()方法,一直等待直到客户端连接到服务器的端口为止;
      3. 服务器等待时,客户端实例化一个Socket对象,指定服务器地址和端口号请求连接;
      4. 客户端的Socket构造函数尝试连接到服务器指定端口,如果成功连接,在客户端创建一个Socket对象使得可以与服务器通信;
      5. 服务器端accept()方法返回一个新的socket引用,使得可以连接到客户端。
  12. 其他

    • 反射

      Java的反射机制是指程序可以访问、检测并修改本身的状态或行为的一种能力,并能根据自身行为的状态和结果调整或修改应用所描述行为的状态和相关的语义。

      通过反射机制,可以访问Java对象的属性、方法、构造方法、关键字、返回值等等。

      实例操作:

      1. 获取Class对象
        • Class c = Class.forName("A");
        • Class c = A.class;
        • Class c = new A().getClass();
      2. 利用Class对象获取新实例

        • Object o = c.newInstance();
      3. 进行相关操作,比如获取属性、方法等等,例如
        • Field fs = c.getDeclaredFields()[0];
    • 并发

      • 并发编程三要素:

      原子性、有序性、可见性。