文章内容整理自JavaGuide

概念与常识

Java特点:

  • 简单易学
  • 面向对象(封装,继承,多态)
  • 平台无关性( Java 虚拟机实现平台无关性)
  • 支持多线程(c++需调用操作系统)
  • 可靠、安全、支持网络编程

JVM、JDK、JRE

JVM:Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。(跨平台

  • 字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

    在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机

    Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点

    Java 程序运行过程

  • 在.class->机器码这一步JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,相对比较慢,所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。

  • 当JIT编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用(编译与解释共存)

    HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分

  • JDK : Java Development Kit ,是功能齐全的Java SDK,包含JRE+编译器(javac)工具(如javadoc和jdb),适用于开发者,能够创建和编译程序。

  • JRE : 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括Java 虚拟机(JVM)Java 类库java命令和其他的一些基础构件。但是,它不能用于创建新程序。

Oracle JDK 和 OpenJDK的对比

  • Open JDK是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的
  • Oracle JDK比OpenJD 更稳定。Open JDK和Oracle JDK 的代码几乎相同,但Oracle JDK有更多的类和一些错误修复
  • 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能

Java 与 c++

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++支持多重继承;虽然 Java 的类不可以多继承,但是通过接口来实现多继承
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
  • C++同时支持方法重载和操作符重载,但是 Java 只支持方法重载

基本语法

字符和字符串

  • 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符
  • 含义: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
  • 占内存大小: 字符常量只占2个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节)
    字符封装类 Character 有一个成员常量 Character.SIZE 值为 16,单位是bits,该值除以 8(1byte=8bits)后就可以得到 2 个字节

    Java数据类型

注释

  • 代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
  • 若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
  • 例如:
// check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

替换为

if (employee.isEligibleForFullBenefits())

去掉复杂的注释,只需要创建一个与注释所言同一事物的函数即可

泛型

  • Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

  • 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

  • Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除

  • 泛型一般有三种使用方式:泛型类、泛型接口、泛型方法

    //泛型类:
    //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
    //在实例化泛型类时,必须指定T的具体类型
    public class Generic<T> {
    
      private T key;
    
      public Generic(T key) {
          this.key = key;
      }
    
      public T getKey() {
          return key;
      }
    }
    //实例化泛型类
    Generic<Integer> genericInteger = new Generic<Integer>(123456);
    //
    //泛型接口 :
    public interface Generator<T> {
      public T method();
    }
    //实现泛型接口,不指定类型:
    class GeneratorImpl<T> implements Generator<T>{
      @Override
      public T method() {
          return null;
      }
    }
    //实现泛型接口,指定类型:
    class GeneratorImpl implements Generator<String>{
      @Override
      public String method() {
          return "hello";
      }
    }
    //泛型方法 :
    public static <E> void printArray(E[] inputArray) {
      for (E element : inputArray) {
          System.out.printf("%s ", element);
      }
      System.out.println();
    }
    //use
    // 创建不同类型数组: Integer, Double 和 Character
    Integer[] intArray = { 1, 2, 3 };
    String[] stringArray = { "Hello", "World" };
    printArray(intArray);
    printArray(stringArray);

常用的通配符为: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

泛型详解

== and equals

  • 对于基本数据类型来说,==比较的是。对于引用数据类型来说,==比较的是对象的内存地址
    因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals:

  • 类没有覆盖 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
  • 类覆盖了 equals()方法 :一般我们都覆盖 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
  • String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
    推荐使用Objects.equals(null,"SnailClimb")比较两个对象,因为Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals

    浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断,建议使用BigDecimal来做比较

hashCode()与equals()

hashCode()

  • hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数
  • 这个哈希码的作用是确定该对象在哈希表中的索引位置
  • hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数
  • 另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。(public native int hashCode();)

作用:减少了 equals 的次数,相应就大大提高了执行速度。

两个对象相等(即equals()返回true),则hashCode()相等;反之则不然

基本数据类型

Java 中有 8 种基本数据类型

  • 6 种数字类型 :byte、short、int、long、float、double
  • 1 种字符类型:char
  • 1 种布尔型:boolean
    java基本数据类型
    这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
    包装类型不赋值就是Null,而基本类型有默认值且不是 Null。

自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来
  • 拆箱:将包装类型转换为基本数据类型
    Integer i = 10;  //装箱 Integer i = Integer.valueOf(10)
    int n = i;   //拆箱 int n = i.intValue()

    所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

    【强制】所有的 POJO 类属性必须使用包装数据类型。
    【强制】RPC 方法的返回值和参数必须使用包装数据类型。
    【推荐】所有的局部变量使用基本数据类型。

常量池与缓存

  • Java 基本类型的包装类的大部分都实现了常量池技术。
  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
  • Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。
  • 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

值传递

Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

下面再总结一下 Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

重载和重写

重载
综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写:
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  • 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  • 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  • 构造方法无法被重写
    方法的重写要遵循“两同两小一大”:
  • “两同”即方法名相同、形参列表相同
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

面向对象和面向过程

  • 面向过程:面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
  • 面向对象:面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。

成员变量与局部变量

  • 成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数
  • 成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰
  • 成员变量和局部变量都能被 final 所修饰
  • 对象存在于堆内存,局部变量则存在于栈内存
  • 成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失
  • 成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值

封装继承多态

封装:

  • 封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息
  • 可以提供一些可以被外界访问的方法来操作属性
    继承:
  • 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类
  • 通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率
    多态:
  • 表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。

String、StringBuffer、StringBuilder

  • String 类中使用final关键字修饰字符数组来保存字符串,private final char value[],所以String对象是不可变的
  • StringBuilder 与 StringBuffer 都继承自AbstractStringBuilder类,在 AbstractStringBuilder中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的

线程安全:

  • String 中的对象是不可变的,也就可以理解为常量,线程安全
  • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全
  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全

性能:

  • 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象
  • StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用
  • 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险

总结:

  • 操作少量的数据: 适用 String
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

Object常见方法

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

抽象类与接口

在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类。这两者有太多相似的地方,又有太多不同的地方。

抽象类

抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:

public abstract void open();

抽象方法必须使用abstract关键字进行修饰。

如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。但抽象类不一定必须含有抽象方法。

因为抽象类中无具体实现的方法,所以不能用抽象类创建对象。

从这里可以看出,抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。

对于一个父类,如果它的某个方法在父类中没有任何意义,必须根据子类的实际需求来进行

不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类就不是abstract类了。

注意,抽象类和普通类的主要有三点区别:

  • 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
  • 抽象类不能用来创建对象;
  • 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。

接口

接口,英文称作interface,在软件工程中,接口泛指供别人调用的方法或者函数。

从这里,我们可以体会到Java语言设计者的初衷,它是对行为的抽象。

接口中可以含有变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误)

而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误)

并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法

接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

允许一个类遵循多个特定的接口。

如果一个非抽象类遵循了某个接口,就必须实现该接口中的所有方法。

对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法。

区别

语法层面上的区别

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口

设计层面

  • 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
  • 继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系
  • 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

反射

反射赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。

  • 优点:可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
  • 缺点:可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时),反射的性能也要稍差点
    动态代理的实现也依赖反射
    为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
    这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

获取class对象的四种方式

//知道具体类的情况下可以使用
Class alunbarClass = TargetObject.class;
//但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
//
//通过 Class.forName()传入类的路径获取
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
//
//通过对象实例instance.getClass()获取
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
//
//通过类加载器xxxClassLoader.loadClass()传入类路径获取
Class clazz = ClassLoader.loadClass("cn.javaguide.TargetObject");
//通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行

例子

public class TargetObject {
    private String value;
    public TargetObject() {
        value = "JavaGuide";
    }
    public void publicMethod(String s) {
        System.out.println("I love " + s);
    }
    private void privateMethod() {
        System.out.println("value is " + value);
    }
}
//
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
        /**
         * 获取TargetObject类的Class对象并且创建TargetObject类实例
         */
        Class<?> tagetClass = Class.forName("cn.javaguide.TargetObject");
        TargetObject targetObject = (TargetObject) tagetClass.newInstance();
        /**
         * 获取所有类中所有定义的方法
         */
        Method[] methods = tagetClass.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method.getName());
        }
        /**
         * 获取指定方法并调用
         */
        Method publicMethod = tagetClass.getDeclaredMethod("publicMethod",
                String.class);

        publicMethod.invoke(targetObject, "JavaGuide");
        /**
         * 获取指定参数并对参数进行修改
         */
        Field field = tagetClass.getDeclaredField("value");
        //为了对类中的参数进行修改我们取消安全检查
        field.setAccessible(true);
        field.set(targetObject, "JavaGuide");
        /**
         * 调用 private 方法
         */
        Method privateMethod = tagetClass.getDeclaredMethod("privateMethod");
        //为了调用private方法我们取消安全检查
        privateMethod.setAccessible(true);
        privateMethod.invoke(targetObject);
    }
}

异常

Java异常类层次结构图

  • 在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。
  • Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。
  • Exception 能被程序本身处理(try-catch), Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。
  • Error 是无法处理的(只能尽量避免)。没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

受检查异常
Java 代码在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就没办法通过编译 。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException...。

不受检查异常(运行时异常)
Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException 及其子类都统称为非受检查异常,例如:NullPointerException、NumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

Throwable 类常用方法

  • public string getMessage():返回异常发生时的简要描述
  • public string toString():返回异常发生时的详细信息
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息

try-catch-finally

  • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块: 用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

try-with-resources

  • 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
  • 关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行
    面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

序列化

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化: 将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程
    序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中

    序列化
    对于不想进行序列化的变量,使用 transient 关键字修饰。
  • transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

枚举

枚举的优势:以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。

  • 我们可以使用==比较枚举类,也可以在switch中使用枚举类

EnumSet

EnumSet 是一种专门为枚举类型所设计的 Set 类型。它是特定 Enum 常量集的非常有效且紧凑的表示形式。

  • 在很多场景中的枚举常量集合操作(如:取子集、增加、删除、containsAll和removeAll批操作)使用EnumSet非常合适;如果需要迭代所有可能的常量则使用Enum.values()。

EnumMap

EnumMap是一个专门化的映射实现,用于将枚举常量用作键。与对应的 HashMap 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组:
EnumMap<Pizza.PizzaStatus, Pizza> map;

设计模式

单例模式

public enum PizzaDeliverySystemConfiguration {
    INSTANCE;
    PizzaDeliverySystemConfiguration() {
        // Initialization configuration which involves
        // overriding defaults like delivery strategy
    }
    private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;

    public static PizzaDeliverySystemConfiguration getInstance() {
        return INSTANCE;
    }
    public PizzaDeliveryStrategy getDeliveryStrategy() {
        return deliveryStrategy;
    }
}
//use
PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy();

策略模式
通常,策略模式由不同类实现同一个接口来实现的。这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。

JSON

使用Jackson库,可以将枚举类型的JSON表示为POJO。

关键字

final

final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:

  • final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法
  • final 修饰的方法不能被重写
  • final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。

使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。

static

  • 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()
  • 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
  • 静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。
  • 静态导包(用来导入类中的静态资源,1.5 之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。

this和super

  • this 关键字用于引用类的当前实例。此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。
  • super 关键字用于从子类访问父类的变量和方法。
  • 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
  • this、super 不能用在 static 方法中。

static{}静态代码块与{}非静态代码块(构造代码块)

  • 相同点:都是在 JVM 加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些 static 变量进行赋值。

  • 不同点: 静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过 Class.forName("ClassDemo")创建 Class 对象的时候也会执行,即 new 或者 Class.forName("ClassDemo") 都会执行静态代码块。而非静态代码块在每 new 一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。

    一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays 类,Character 类,String 类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.

    public class Test {
      public Test() {
          System.out.print("默认构造方法!--");
      }
    
      //非静态代码块
      {
          System.out.print("非静态代码块!--");
      }
    
      //静态代码块
      static {
          System.out.print("静态代码块!--");
      }
    
      private static void test() {
          System.out.print("静态方法中的内容! --");
          {
              System.out.print("静态方法中的代码块!--");
          }
    
      }
    
      public static void main(String[] args) {
          Test test = new Test();
          Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!--
      }
    }
    //静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!--

动态代理

代理模式

简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

代理模式的主要作用是扩展(增强)目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

举例:你找了小红来帮你问话,小红就可以看作是代理你的代理对象,代理的行为(方法)是问话。

代理模式有静态代理和动态代理两种实现方式

静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

静态代理实现步骤:

  • 定义一个接口及其实现类;
  • 创建一个代理类同样实现这个接口
  • 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

动态代理

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。

JDK动态代理

在 Java 动态代理机制中 InvocationHandler 接口和 Proxy类是核心。

Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

    public static Object newProxyInstance(ClassLoader loader, //类加载器,用于加载代理对象
                                          Class<?>[] interfaces,  //被代理类实现的一些接口
                                          InvocationHandler h)  //实现了 InvocationHandler 接口的对象
        throws IllegalArgumentException
    {
        //
    }

步骤:

  • 定义一个接口及其实现类;
  • 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  • 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

CGLIB动态代理

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。我们可以用 CGLIB 动态代理机制来避免。

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。

你需要自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。

public interface MethodInterceptor
extends Callback{
    // 拦截被代理类中的方法
    public Object intercept(Object obj,   //被代理的对象(需要增强的对象)
                           java.lang.reflect.Method method,    //被拦截的方法(需要增强的方法)
                           Object[] args,   //方法入参
                           MethodProxy proxy   //用于调用原始方法
    ) throws Throwable;
}

你可以通过 `Enhancer类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor 中的 intercept 方法。

步骤:

  • 定义一个类;
  • 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  • 通过 Enhancer 类的 create()创建代理类;

对比

  • JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  • 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显

静态代理和动态代理的对比

  • 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
  • JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

I/O

I/O(Input/Outpu) 即输入/输出。
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。

输入设备向计算机输入数据,输出设备接收计算机输出的数据。

从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

我们再先从应用程序的角度来解读一下 I/O。

根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space)内核空间(Kernel space )

像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。

并且,用户空间的程序不能直接访问内核空间。

当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。

因此,用户进程想要执行 IO 操作的话,必须通过系统调用来间接访问内核空间

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 I/O 调用后,会经历两个步骤:

  • 内核等待 I/O 设备准备好数据
  • 内核将数据从内核空间拷贝到用户空间。

常见I/O模型

UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O

Java中常见I/O模型

BIO

BIO(Blocking I/O) 属于同步阻塞 IO 模型

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。


在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO

NIO(Non-blocking/New I/O)提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型

同步非阻塞 IO 模型:

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

这个时候,I/O 多路复用模型 就上场了。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

AIO

AIO(Asynchronous I/O) 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

总结

最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。

I/O流

  • 按照流的流向分,可以分为输入流输出流
  • 按照操作单元划分,可以划分为字节流字符流
  • 按照流的角色划分为节点流处理流
    InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
    IO-操作方式分类
    IO-操作对象分类

既然有了字节流,为什么还要有字符流?

字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

最后修改:2021 年 11 月 10 日