java基础(部分)

数据类型

1.基本数据类型

  • 布尔型 : boolean
  • 数值型
    • 字节型: byte
    • 整型:
      • short
      • int
      • long
    • 浮点型:
      • float
      • double
  • 字符型 : char

2.引用数据类型

  • 数组
  • 接口

变量初始化

局部变量

  • 使用前必须先初始化,否则编译器不允许你使用它

成员变量或者静态变量时

  • 使用前可以不进行初始化,它们会有一个默认值

浮点类型的精度

float

  • 单精度是这样的格式,1 位符号,8 位指数,23 位小数,有效位数为 7 位。

double

  • 双精度是这样的格式,1 位符号,11 位指数,52 为小数,有效位数为 16 位。

BigDecimal (引用类型)

  • 可以表示一个任意大小且精度完全准确的浮点数。

闰年

  • 被 4 整除但不能被 100 整除或者被 400 整除的年份是闰年

关键字

  1. abstract: 用于声明抽象类,以及抽象方法
  2. boolean: 用于将变量声明为布尔类型, 只有true和false
  3. break: 用于中断循环或switch语句
  4. byte: 用于声明一个可以容纳8个比特的变量
  5. case: 用于在switch语句中标记条件的值
  6. catch: 用于捕获try语句中的异常
  7. char: 用于声明一个可以容纳无字符16位比特的Unicode 字符的变量
  8. class: 用于声明一个类
  9. continue: 用于继续下一个循环, 可以在指定条件下跳过其余代码
  10. default: 用于指定switch语句中除去case条件之外的默认代码块
  11. do: 通常和while关键字配合使用, do后紧跟循环体
  12. double: 用于声明一个可以容纳64位浮点数的变量
  13. else: 用于指示if语句的备用分支
  14. enum: 用于定义一组固定的常量(枚举)
  15. extends: 用于指示一个类是从另一个类或接口继承的
  16. final: 用于指示该变量是不可更改的
  17. finally: 和 try-catch 配合使用, 表示无论是否处理异常,总是执行finally块中的代码
  18. float: 用于声明一个可以容纳 32 位浮点数的变量。
  19. for: 用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环。
  20. if: 用于指定条件,如果条件为真,则执行对应代码。
  21. implements: 用于实现接口。
  22. import: 用于导入对应的类或者接口。
  23. instanceof: 用于判断对象是否属于某个类型(class)。
  24. int: 用于声明一个可以容纳 32 位带符号的整数变量。
  25. interface: 用于声明接口。
  26. long: 用于声明一个可以容纳 64 位整数的变量。
  27. native: 用于指定一个方法是通过调用本机接口(非 Java)实现的。
  28. new: 用于创建一个新的对象。
  29. null: 如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和空指针异常息息相关。
  30. package: 用于声明类所在的包。
  31. private: 一个访问权限修饰符,表示方法或变量只对当前类可见。
  32. protected: 一个访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见。
  33. public: 一个访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public。
  34. return: 用于在代码执行完成后返回(一个值)。
  35. short: 用于声明一个可以容纳 16 位整数的变量。
  36. static: 表示该变量或方法是静态变量或静态方法。
  37. strictfp: 并不常见,通常用于修饰一个方法,确保方法体内的浮点数运算在每个平台上执行的结果相同。
  38. super: 可用于调用父类的方法或者字段。
  39. switch: 通常用于三个(以上)的条件判断。
  40. synchronized: 用于指定多线程代码中的同步方法、变量或者代码块。
  41. this: 可用于在方法或构造函数中引用当前对象。
  42. throw: 主动抛出异常。
  43. throws: 用于声明异常。
  44. transient: 修饰的字段不会被序列化。
  45. try: 用于包裹要捕获异常的代码块。
  46. void: 用于指定方法没有返回值。
  47. volatile: 保证不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。
  48. while: 如果循环次数不固定,建议使用 while 循环。

面向对象编程

01、面向过程和面向对象

  • 面向过程:
    • 面向过程是流程化的,一步一步,上一步做完了,再做下一步。
  • 面向对象:
    • 面向对象是模块化的,我做我的,你做你的,我需要你做的话,我就告诉你一声。我不需要知道你到底怎么做,只看功劳不看苦劳。

02、类

对象可以是现实中看得见的任何物体,比如说,一只特立独行的猪;也可以是想象中的任何虚拟物体,比如说能七十二变的孙悟空。

Java 通过类(class)来定义这些物体,这些物体有什么状态,通过字段来定义,比如说比如说猪的颜色是纯色还是花色;这些物体有什么行为,通过方法来定义,比如说猪会吃,会睡觉。

一个类可以包含:

  • 字段(Filed)
  • 方法(Method)
  • 构造方法(Constructor)

03、关于对象

1) 抽象的历程

所有编程语言都是一种抽象,甚至可以说,我们能够解决的问题的复杂程度取决于抽象的类型和质量。

Smalltalk 是历史上第一门获得成功的面向对象语言,也为 Java 提供了灵感。它有 5 个基本特征:

  • 万物皆对象。
  • 一段程序实际上就是多个对象通过发送消息的方式来告诉彼此该做什么。
  • 通过组合的方式,可以将多个对象封装成其他更为基础的对象。
  • 对象是通过类实例化的。
  • 同一类型的对象可以接收相同的消息。

状态+行为+标识=对象,每个对象在内存中都会有一个唯一的地址。

2) 对象具有接口

所有的对象,都可以被归为一类,并且同一类对象拥有一些共同的行为和特征。在 Java 中,class 关键字用来定义一个类型。

创建抽象数据类型是面向对象编程的一个基本概念。你可以创建某种类型的变量,Java 中称之为对象或者实例,然后你就可以操作这些变量,Java 中称之为发送消息或者发送请求,最后对象决定自己该怎么做。

类描述了一系列具有相同特征和行为的对象,从宽泛的概念上来说,类其实就是一种自定义的数据类型。

一旦创建了一个类,就可以用它创建任意多个对象。面向对象编程语言遇到的最大一个挑战就是,如何把现实/虚拟的元素抽象为 Java 中的对象。

对象能够接收什么样的请求是由它的接口定义的。具体是怎么做到的,就由它的实现方法来实现。

3) 访问权限修饰符

类的创建者有时候也被称为 API 提供者,对应的,类的使用者就被称为 API 调用者。

JDK 就给我们提供了 Java 的基础实现,JDK 的作者也就是基础 API 的提供者(Java 多线程部分的作者 Doug Lea 是被 Java 程序员敬佩的一个大佬),我们这些 Java 语言的使用者,说白了就是 JDK 的调用者。

当然了,假如我们也提供了新的类给其他调用者,我们也就成为了新的创建者。

API 创建者在创建新的类的时候,只暴露必要的接口,而隐藏其他所有不必要的信息,之所以要这么做,是因为如果这些信息对调用者是不可见的,那么创建者就可以随意修改隐藏的信息,而不用担心对调用者的影响。

这里就必须要讲到 Java 的权限修饰符。

访问权限修饰符的第一个作用是,防止类的调用者接触到他们不该接触的内部实现;第二个作用是,让类的创建者可以轻松修改内部机制而不用担心影响到调用者的使用。

  • public
  • private
  • protected

还有一种“默认”的权限修饰符,是缺省的,它修饰的类可以访问同一个包下面的其他类。

4) 组合

我们可以把一个创建好的类作为另外一个类的成员变量来使用,利用已有的类组成成一个新的类,被称为“复用”,组合代表的关系是 has-a 的关系。

5) 继承

继承是 Java 中非常重要的一个概念,子类继承父类,也就拥有了父类中 protected 和 public 修饰的方法和字段,同时,子类还可以扩展一些自己的方法和字段,也可以重写继承过来方法。

常见的例子,就是形状可以有子类圆形、方形、三角形,它们的基础接口是相同的,比如说都有一个 draw() 的方法,子类可以继承这个方法实现自己的绘制方法。

如果子类只是重写了父类的方法,那么它们之间的关系就是 is-a 的关系,但如果子类增加了新的方法,那么它们之间的关系就变成了 is-like-a 的关系。

6)多态

略…

变量

01、局部变量

在方法体内声明的变量被称为局部变量,该变量只能在该方法内使用,类中的其他方法并不知道该变量

声明局部变量时的注意事项:

  • 局部变量声明在方法、构造方法或者语句块中。
  • 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,将会被销毁。
  • 访问修饰符不能用于局部变量。
  • 局部变量只在声明它的方法、构造方法或者语句块中可见。
  • 局部变量是在栈上分配的。
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

02、成员变量

在类内部但在方法体外声明的变量称为成员变量,或者实例变量。之所以称为实例变量,是因为该变量只能通过类的实例(对象)来访问

声明成员变量时的注意事项:

  • 成员变量声明在一个类中,但在方法、构造方法和语句块之外。
  • 当一个对象被实例化之后,每个成员变量的值就跟着确定。
  • 成员变量在对象创建的时候创建,在对象被销毁的时候销毁。
  • 成员变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息。
  • 成员变量可以声明在使用前或者使用后。
  • 访问修饰符可以修饰成员变量。
  • 成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见;成员变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。

03、静态变量

通过 static 关键字声明的变量被称为静态变量(类变量),它可以直接被类访问

声明静态变量时的注意事项:

  • 静态变量在类中以 static 关键字声明,但必须在方法构造方法和语句块之外。
  • 无论一个类创建了多少个对象,类只拥有静态变量的一份拷贝。
  • 静态变量除了被声明为常量外很少使用。
  • 静态变量储存在静态存储区。
  • 静态变量在程序开始时创建,在程序结束时销毁。
  • 与成员变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型。
  • 静态变量的默认值和实例变量相似。
  • 静态变量还可以在静态语句块中初始化。

04、常量

在 Java 中,有些数据的值是不会发生改变的,这些数据被叫做常量——使用 final 关键字修饰的成员变量。常量的值一旦给定就无法改变!


常量在程序运行过程中主要有 2 个作用:

  • 代表常数,便于修改(例如:圆周率的值,final double PI = 3.14
  • 增强程序的可读性(例如:常量 UP、DOWN 用来代表上和下,final int UP = 0

声明常量时的注意事项: Java 要求常量名必须大写

权限限定符

修饰类的方法和变量

  • 默认访问权限(包访问权限):如果一个类的方法或变量被包访问权限修饰,也就意味着只能在同一个包中的其他类中显示地调用该类的方法或者变量,在不同包中的类中不能显式地调用该类的方法或变量。
  • private:如果一个类的方法或者变量被 private 修饰,那么这个类的方法或者变量只能在该类本身中被访问,在类外以及其他类中都不能显式的进行访问。
  • protected:如果一个类的方法或者变量被 protected 修饰,对于同一个包的类,这个类的方法或变量是可以被访问的。对于不同包的类,只有继承于该类的类才可以访问到该类的方法或者变量。
  • public:被 public 修饰的方法或者变量,在任何地方都是可见的。

方法

01) 方法结构:

返回类型:方法返回的数据类型,可以是基本数据类型、对象和集合,如果不需要返回数据,则使用 void 关键字。

方法名:方法名最好反应出方法的功能,比如,我们要创建一个将两个数字相减的方法,那么方法名最好是 subtract。

方法名最好是一个动词,并且以小写字母开头。如果方法名包含两个以上单词,那么第一个单词最好是动词,然后是形容词或者名词,并且要以驼峰式的命名方式命名。比如:

  • 一个单词的方法名:sum()
  • 多个单词的方法名:stringComparision()

一个方法可能与同一个类中的另外一个方法同名,这被称为方法重载。

参数:参数被放在一个圆括号内,如果有多个参数,可以使用逗号隔开。参数包含两个部分,参数类型和参数名。如果方法没有参数,圆括号是空的。

方法签名:每一个方法都有一个签名,包括方法名和参数。

方法体:方法体放在一对花括号内,把一些代码放在一起,用来执行特定的任务。

02) 方法有哪几种?

方法可以分为两种,一种叫预先定义方法,一种叫用户自定义方法。

  1. 预先定义方法

    ​ Java 提供了大量预先定义好的方法供我们调用,也称为标准类库方法,或者内置方法。

  2. 用户自定义方法

    当预先定义方法无法满足我们的要求时,就需要自定义一些方法

03) 什么是实例方法

没有使用 static 关键字修饰,但在类中声明的方法被称为实例方法,在调用实例方法之前,必须创建类的对象。

实例方法有两种特殊类型:

  • getter 方法
  • setter 方法

getter 方法用来获取私有变量(private 修饰的字段)的值,setter 方法用来设置私有变量的值。

04) 什么是抽象方法

没有方法体的方法被称为抽象方法,它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 atstract 关键字创建抽象方法和抽象类。

当一个类继承了抽象类后, 就必须重写抽象方法;

构造方法

“在 Java 中,构造方法是一种特殊的方法,当一个类被实例化的时候,就会调用构造方法。只有在构造方法被调用的时候,对象才会被分配内存空间。每次使用 new 关键字创建对象的时候,构造方法至少会被调用一次。”

“如果你在一个类中没有看见构造方法,并不是因为构造方法不存在,而是被缺省了,编译器会给这个类提供一个默认的构造方法。往大的方面说,就是,Java 有两种类型的构造方法:无参构造方法和有参构造方法。”

“注意,之所以叫它构造方法,是因为对象在创建的时候,需要通过构造方法初始化值——就是描写对象的那些状态,对应的是类中的字段。”

01、创建构造方法的规则有哪些

构造方法必须符合以下规则:

  • 构造方法的名字必须和类名一样;
  • 构造方法没有返回类型,包括 void;
  • 构造方法不能是抽象的、静态的、最终的、同步的,也就是说,构造方法不能通过 abstract、static、final、synchronized 关键字修饰。

简单解析一下最后一条规则。

  • 由于构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义;

  • 构造方法用于初始化一个对象,所以用 static 修饰没有意义;

  • 多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 修饰没有必要。

值得注意的是,如果用 void 声明构造方法的话,编译时不会报错,但 Java 会把这个所谓的“构造方法”当成普通方法来处理。

eg:

java源码:

public class Demo {
    void Demo(){ }
}

字节码:

public class Demo {
    public Demo() {
    }

    void Demo() {
    }
}

02、什么是默认构造方法

如果一个构造方法中没有任何参数,那么它就是一个默认构造方法,也称为无参构造方法。

通常情况下,无参构造方法是可以缺省的,我们开发者并不需要显式的声明无参构造方法,把这项工作交给编译器更轻松一些。

默认构造方法的作用

默认构造方法的目的主要是为对象的字段提供默认值

03、什么是有参构造方法

有参数的构造方法被称为有参构造方法,参数可以有一个或多个。有参构造方法可以为不同的对象提供不同的值。当然,也可以提供相同的值。

如果没有有参构造方法的话,就需要通过 setter 方法给字段赋值了。

04、如何重载构造方法

在 Java 中,构造方法和方法类似,只不过没有返回类型。它也可以像方法一样被重载。构造方法的重载也很简单,只需要提供不同的参数列表即可。编译器会通过参数的数量来决定应该调用哪一个构造方法。

05、构造方法和方法有什么区别

构造方法和方法之间的区别还是蛮多的,比如说下面这些:

方法构造方法
方法反应了对象的行为构造方法用于初始化对象的字段
方法可以有返回类型构造方法没有返回类型
方法的调用是明确的,开发者通过代码决定调用哪一个构造方法的调用是隐式的,通过编译器完成
方法在任何情况下都不由编译器提供如果没有明确提供无参构造方法,编译器会提供
方法名可以和类名相同,也可以不同构造方法的名称必须和类名相同

06、如何复制对象

复制一个对象可以通过下面三种方式完成:

  • 通过构造方法
  • 通过对象的值
  • 通过 Object 类的 clone() 方法

1) 通过构造方法

public class CopyConstrutorPerson {
    private String name;
    private int age;

    public CopyConstrutorPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public CopyConstrutorPerson(CopyConstrutorPerson person) {
        this.name = person.name;
        this.age = person.age;
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) {
        CopyConstrutorPerson p1 = new CopyConstrutorPerson(",18);
        p1.out();

        CopyConstrutorPerson p2 = new CopyConstrutorPerson(p1);
        p2.out();
    }
}

在上面的例子中,有一个参数为 CopyConstrutorPerson 的构造方法,可以把该参数的字段直接复制到新的对象中,这样的话,就可以在 new 关键字创建新对象的时候把之前的 p1 对象传递过去。

2) 通过对象的值

public class CopyValuePerson {
    private String name;
    private int age;

    public CopyValuePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public CopyValuePerson() {
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) {
        CopyValuePerson p1 = new CopyValuePerson("菜鸡学java",18);
        p1.out();

        CopyValuePerson p2 = new CopyValuePerson();
        p2.name = p1.name;
        p2.age = p1.age;
        
        p2.out();
    }
}

这种方式比较粗暴,直接拿 p1 的字段值复制给 p2 对象(p2.name = p1.name)。

3) 通过Object类的clone()方法

public class ClonePerson implements Cloneable {
    private String name;
    private int age;

    public ClonePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        ClonePerson p1 = new ClonePerson("菜鸡学java",18);
        p1.out();

        ClonePerson p2 = (ClonePerson) p1.clone();
        p2.out();
    }
}

通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())。

07、ending

  1. 构造方法真的不返回任何值吗?

    构造方法虽然没有返回值,但返回的是类的对象。

  2. 构造方法只能完成字段初始化的工作吗?

    初始化字段只是构造方法的一种工作,它还可以做更多,比如启动线程,调用其他方 法等。

代码初始化块

  • 类实例化的时候执行代码初始化块;
  • 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
  • 代码初始化块里的执行顺序是从前到后的。

“这些规则不用死记硬背,大致了解一下就行了。我们继续来看下面这段代码。”话音刚落,我就在新版的 IDEA 中噼里啪啦地敲了起来,新版真香。

class A {
    A () {
        System.out.println("父类构造方法");
    }
}
public class B extends A{
    B() {
        System.out.println("子类构造方法");
    }

    {
        System.out.println("代码初始化块");
    }

    public static void main(String[] args) {
        new B();
    }
}

“来看一下输出结果。”

父类构造方法
代码初始化块
子类构造方法

“在默认情况下,子类的构造方法在执行的时候会主动去调用父类的构造方法。也就是说,其实是构造方法先执行的,再执行的代码初始化块。”

“这个例子再次印证了之前的第二条规则:代码初始化块是放在构造方法中执行的,只不过比较靠前。”

抽象类

定义抽象类的时候需要用到关键字 abstract,放在 class 关键字前,就像下面这样。

关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的。

抽象类是不能实例化的,尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。

虽然抽象类不能实例化,但可以有子类。子类通过 extends 关键字来继承抽象类。就像下面这样。

public class BasketballPlayer extends AbstractPlayer {
}

如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。

抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样:

public abstract class AbstractPlayer {
    abstract void play();
    
    public void sleep() {
        System.out.println("运动员也要休息而不是挑战极限");
    }
}

抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类 AbstractPlayer 中定义了 play() 方法,子类 BasketballPlayer 中就必须实现。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是张伯伦,篮球场上得过 100 分");
    }
}

抽象类的应用场景

第一种场景

当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。

第二种场景

当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。比如说,AbstractPlayer 抽象类中定义了一个抽象方法 play(),表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。

为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单,只有一个“Hello World”,现在需要有一个读取器将内容从文件中读取出来,最好能按照大写的方式,或者小写的方式来读。

这时候,最好定义一个抽象类 BaseFileReader:

abstract class BaseFileReader {
    protected Path filePath;

    protected BaseFileReader(Path filePath) {
        this.filePath = filePath;
    }

    public List<String> readFile() throws IOException {
        return Files.lines(filePath)
                .map(this::mapFileLine).collect(Collectors.toList());
    }

    protected abstract String mapFileLine(String line);
}
  • filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问到。
  • readFile() 方法用来读取文件,方法体里面调用了抽象方法 mapFileLine()——需要子类来扩展实现大小写的不同读取方式。

在我看来,BaseFileReader 类设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。

小写的方式:

class LowercaseFileReader extends BaseFileReader {
    protected LowercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toLowerCase();
    }
}

大写的方式:

class UppercaseFileReader extends BaseFileReader {
    protected UppercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toUpperCase();
    }
}

从文件里面一行一行读取内容的代码被子类复用了。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

接口

如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种上一篇的主角——抽象类,另外一种就是今天的主角——接口。”

接口通过 interface 关键字来定义,它可以包含一些常量和方法,来看下面这个示例。

java源码:

public interface Electronic {
    // 常量
    
    

    // 抽象方法
    int getElectricityUse();

    // 静态方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默认方法
    default void printDescription() {
        System.out.println("电子");
    }
}

java字节码:

public interface Electronic
{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    {
        return electtronicType.equals("LED");
    }

    public void printDescription()
    {
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

发现没?接口中定义的所有变量或者方法,都会自动添加上 public 关键字。

接下来,我来一一解释下 Electronic 接口中的核心知识点。

1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符(注意看一下反编译后的字节码),也就是说上例中的 LED 变量其实就是一个常量。

Every field declaration in the body of an interface is implicitly public, static, and final.

换句话说,接口可以用来作为常量类使用,还能省略掉 public static final,看似不错的一种选择,对吧?

不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。

2)没有使用 privatedefault 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。也就是说上例中的 getElectricityUse() 其实是一个抽象方法,没有方法体——这是定义接口的本意。

3)从 Java 8 开始,接口中允许有静态方法,比如说上例中的 isEnergyEfficient() 方法。

静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说 Electronic.isEnergyEfficient("LED")

接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。

4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说上例中的 printDescription() 方法,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。

为什么要在接口中定义默认方法呢?

允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 default 方法的帮助下,我们就必须挨个对实现类进行修改。

由之前的例子我们就可以得出下面这些结论:

  • 接口中允许定义变量
  • 接口中允许定义抽象方法
  • 接口中允许定义静态方法(Java 8 之后)
  • 接口中允许定义默认方法(Java 8 之后)

除此之外,我们还应该知道:

1)接口不允许直接实例化,否则编译器会报错。

2)接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 java.io 包下。

Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。

3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。

4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。

5)接口的变量是隐式 public static final(常量),所以其值无法改变。

接口可以做什么呢

第一,使某些实现类具有我们想要的功能,比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。

Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 Object.clone() 方法,否则会抛出 CloneNotSupportedException。

第二,Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。

如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题,见下图。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YJYzDxG-1658832846990)(C:\Users\26308\Desktop\学习记录\java学习\java语言\images\菱形继承.png)]

简单解释下,ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重写的方法时,就不知道该调用 ClassA 的方法,还是 ClassB 的方法。

接口没有这方面的困扰。

在某种形式上,接口实现了多重继承的目的:现实世界里,猪的确只会跑,但在雷军的眼里,站在风口的猪就会飞,这就需要赋予这只猪更多的能力,通过抽象类是无法实现的,只能通过接口。

第三,实现多态。

什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。

多态可以通过继承(extends)的关系实现,也可以通过接口的形式实现。

eg:

Shape 接口表示一个形状。

public interface Shape {
    String name();
}

Circle 类实现了 Shape 接口,并重写了 name() 方法。

public class Circle implements Shape {
    @Override
    public String name() {
        return "圆";
    }
}

Square 类也实现了 Shape 接口,并重写了 name() 方法。

public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}

然后来看测试类。

List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 shape.name() 方法的时候,Java 虚拟机知道该去调用 Circle 的 name() 方法还是 Square 的 name() 方法。

说一下多态存在的 3 个前提:

1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。

2、子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法。

3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。

然后,我们来看一下测试结果:


正方形

也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 name() 方法的时候,它知道 Circle 对象应该调用 Circle 类的 name() 方法,Square 对象应该调用 Square 类的 name() 方法。

1)语法层面上

  • 接口中不能有 private 和 protected 修饰的方法,抽象类中可以有。
  • 接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。
  • 一个类只能继承一个抽象类,但却可以实现多个接口。

2)设计层面上

抽象类是对类的一种抽象,继承抽象类的子类和抽象类本身是一种 is-a 的关系。

接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 Serializable 接口,从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 is-a 的关系。

内部类

内部类简介

在 Java 中,可以将一个类定义在另外一个类里面或者一个方法里面,这样的类叫做内部类。

一般来说,内部类分为成员内部类、局部内部类、匿名内部类和静态内部类。

为什么要使用内部类?

在《Think in java》中有这样一句话:

使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。

使用内部类还能够为我们带来如下特性(摘自《Think in java》):

  • 1、内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
  • 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
  • 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
  • 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
  • 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。

static

“static 关键字的作用可以用一句话来描述:‘方便在没有创建对象的情况下进行调用,包括变量和方法’。也就是说,只要类被加载了,就可以通过类名进行访问。

01、静态变量

“如果在声明变量的时候使用了 static 关键字,那么这个变量就被称为静态变量。静态变量只在类加载的时候获取一次内存空间,这使得静态变量很节省内存空间。

02、静态方法

“静态方法有以下这些特征。”

  • 静态方法属于这个类而不是这个类的对象;
  • 调用静态方法的时候不需要创建这个类的对象;
  • 静态方法可以访问静态变量。

需要注意的是,静态方法不能访问非静态变量和调用非静态方法。

03、静态代码块

用一个 static 关键字,外加一个大括号括起来的代码被称为静态代码块。

静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行。

静态代码块在初始集合的时候,真的非常有用。在实际的项目开发中,通常使用静态代码块来加载配置文件到内存当中。

eg:

public class StaticBlockDemo {
    public static List<String> writes = new ArrayList<>();

    static {
        writes.add("Java");
        writes.add("Java1");
        writes.add("Java2");

        System.out.println("第一块");
    }

    static {
        writes.add("Java3");
        writes.add("Java4");

        System.out.println("第二块");
    }
}

04、静态内部类

eg:

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。不过,创建单例更优雅的一种方式是使用枚举,以后再讲给你听。

需要注意的是。

第一,静态内部类不能访问外部类的所有成员变量;

第二,静态内部类可以访问外部类的所有静态变量,包括私有静态变量。

第三,外部类不能声明为 static。

this&super

this 关键字有很多种用法,其中最常用的一个是,它可以作为引用变量,指向当前对象。

除此之外, this 关键字还可以完成以下工作。

  • 调用当前类的方法;
  • this() 可以调用当前类的构造方法;
  • this 可以作为参数在方法中传递;
  • this 可以作为参数在构造方法中传递;
  • this 可以作为方法的返回值,返回当前类的对象。

01、指向当前对象

02、调用当前类的方法

我们可以在一个类中使用 this 关键字来调用另外一个方法,如果没有使用的话,编译器会自动帮我们加上。

03、调用当前类的构造方法

使用语法:this(对应参数列表)

注意:调用的构造方法必须是已存在的并且this() 必须放在构造方法的第一行,否则就报错了

04、作为参数在方法中传递

public class ThisAsParam {
    void method1(ThisAsParam p) {
        System.out.println(p);
    }

    void method2() {
        method1(this);
    }

    public static void main(String[] args) {
        ThisAsParam thisAsParam = new ThisAsParam();
        System.out.println(thisAsParam);
        thisAsParam.method2();
    }
}

this 关键字可以作为参数在方法中传递,此时,它指向的是当前类的对象。

输出结果:

com.itwanger.twentyseven.ThisAsParam@77459877
com.itwanger.twentyseven.ThisAsParam@77459877

method2() 调用了 method1(),并传递了参数 this,method1() 中打印了当前对象的字符串。 main() 方法中打印了 thisAsParam 对象的字符串。从输出结果中可以看得出来,两者是同一个对象。

05、作为参数在构造方法中传递

public class ThisAsConstrutorParam {
    int count = 10;

    ThisAsConstrutorParam() {
        Data data = new Data(this);
        data.out();
    }

    public static void main(String[] args) {
        new ThisAsConstrutorParam();
    }
}

class Data {
    ThisAsConstrutorParam param;
    Data(ThisAsConstrutorParam param) {
        this.param = param;
    }

    void out() {
        System.out.println(param.count);
    }
}

在构造方法 ThisAsConstrutorParam() 中,我们使用 this 关键字作为参数传递给了 Data 对象,它其实指向的就是 new ThisAsConstrutorParam() 这个对象。

this 关键字也可以作为参数在构造方法中传递,它指向的是当前类的对象。当我们需要在多个类中使用一个对象的时候,这非常有用。

06、作为方法的返回值

public class ThisAsMethodResult {
    ThisAsMethodResult getThisAsMethodResult() {
        return this;
    }
    
    void out() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        new ThisAsMethodResult().getThisAsMethodResult().out();
    }
}

getThisAsMethodResult() 方法返回了 this 关键字,指向的就是 new ThisAsMethodResult() 这个对象,所以可以紧接着调用 out() 方法——达到了链式调用的目的,这也是 this 关键字非常经典的一种用法。

链式调用的形式在 JavaScript 代码更加常见。

需要注意的是,this 关键字作为方法的返回值的时候,方法的返回类型为类的类型。

07、super关键字

“super 关键字的用法主要有三种。”

  • 指向父类对象;
  • 调用父类的方法;
  • super() 可以调用父类的构造方法。

“其实和 this 有些相似,只不过用意不大相同。”我端起水瓶,咕咚咕咚又喝了几大口,好渴。“每当创建一个子类对象的时候,也会隐式的创建父类对象,由 super 关键字引用。”

“如果父类和子类拥有同样名称的字段,super 关键字可以用来访问父类的同名字段。”

eg:

public class ReferParentField {
    public static void main(String[] args) {
        new Dog().printColor();
    }
}

class Animal {
    String color = "白色";
}

class Dog extends Animal {
    String color = "黑色";

    void printColor() {
        System.out.println(color);
        System.out.println(super.color);
    }
}

父类 Animal 中有一个名为 color 的字段,子类 Dog 中也有一个名为 color 的字段,子类的 printColor() 方法中,通过 super 关键字可以访问父类的 color。

结果:

黑色
白色

“当子类和父类的方法名相同时,可以使用 super 关键字来调用父类的方法。换句话说,super 关键字可以用于方法重写时访问到父类的方法。”

“当然了,在默认情况下,super() 是可以省略的,编译器会主动去调用父类的构造方法。也就是说,子类即使不使用 super() 主动调用父类的构造方法,父类的构造方法仍然会先执行。”

super() 也可以用来调用父类的有参构造方法,这样可以提高代码的可重用性。”

final

  • 修饰变量 (final 修饰的成员变量必须有一个默认值,否则编译器将会提醒没有初始化)
  • 修饰类
  • 修饰方法 (被 final 修饰的方法不能被重写。如果我们在设计一个类的时候,认为某些方法不应该被重写,就应该把它设计成 final 的, eg:Thread类中的isAlive() native 方法)

“final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写。”

“有时候,我们还会用 final 关键字来修饰参数,它意味着参数在方法体内不能被再修改。”

String 类要设计成 final

“原因大致有 3 个。”

  • 为了实现字符串常量池
  • 为了线程安全
  • 为了 HashCode 的不可变性

不过,类是 final 的,并不意味着该类的对象是不可变的。

X

不可变类

“把一个类设计成 final 的,有其安全方面的考虑,但不应该故意为之,因为把一个类定义成 final 的,意味着它没办法继承,假如这个类的一些方法存在一些问题的话,我们就无法通过重写的方式去修复它。”

instanceof

作用: 判断对象是否符合指定的类型

语法:

(object) instanceof (type)

意思就是类型不匹配,不能转换,我们使用 instanceof 比较的目的,也就是希望如果结果为 true 的时候能进行类型转换。

Java 是一门面向对象的编程语言,也就意味着除了基本数据类型,所有的类都会隐式继承 Object 类

对象为null呢

System.out.println(null instanceof Object);

只有对象才会有 null 值,所以编译器是不会报错的,只不过,对于 null 来说,instanceof 的结果为 false。因为所有的对象都可以为 null,所以也不好确定 null 到底属于哪一个类。

通常,我们是这样使用 instanceof 操作符的。

// 先判断类型
if (obj instanceof String) {
    // 然后强制转换
    String s = (String) obj;
    // 然后才能使用
}

先用 instanceof 进行类型判断,然后再把 obj 强制转换成我们期望的类型再进行使用。

JDK 16 的时候,instanceof 模式匹配转了正,意味着使用 instanceof 的时候更便捷了。

if (obj instanceof String s) {
    // 如果类型匹配 直接使用 s
}

不可变对象

01、什么是不可变类

一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。

自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。

为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。

假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。

02、常见的不可变类

提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢?

1)常量池的需要

字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。

2)hashCode 的需要

因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。

3)线程安全

就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。

因此,当我们调用 String 类的任何方法(比如说 trim()substring()toLowerCase())时,总会返回一个新的对象,而不影响之前的值。

除了 String 类,包装器类 Integer、Long 等也是不可变类。

03、手撸不可变类

1)确保类是 final 的,不允许被其他类继承。

2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。

3)不要提供任何 setter 方法。

4)如果要修改类的状态,必须返回一个新的对象。

eg:

public final class Writer {
    private final String name;
    private final int age;

    public Writer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。

04、总结

不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。

可变参数

当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法

泛型

Java 在 1.5 时增加了泛型机制,据说专家们为此花费了 5 年左右的时间(听起来很不容易)。有了泛型之后,尤其是对集合类的使用,就变得更规范了。

使用类型参数解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。

泛型方法语法糖:

/**
<T> 表示泛型方法
T[] 方法返回类型(可无)
T[] a  方法参数类型(可无)
*/

public <T> T[] toArray(T[] a) {
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    }

泛型变量的限定符 extends

extends 可以缩小泛型的类型范围。

Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object

类型擦除会有什么问题吗?

eg:

public class Cmower {
    
    public static void method(Arraylist<String> list) {
        System.out.println("Arraylist<String> list");
    }

    public static void method(Arraylist<Date> list) {
        System.out.println("Arraylist<Date> list");
    }

}

在浅层的意识上,我们会想当然地认为 Arraylist<String> listArraylist<Date> list 是两种不同的类型,因为 String 和 Date 是不同的类。

但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”):

>Erasure of method method(Arraylist<String>) is the same as another method in type 
 Cmower
>
>Erasure of method method(Arraylist<Date>) is the same as another method in type 
 Cmower

通配符

通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 extends 限定子类,也可以使用关键字 super 限定父类。

<? extends Wanger> 形式的通配符,可以实现泛型的向上转型

eg:

Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());

Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);

list2 的类型是 Arraylist<? extends Wanger>,翻译一下就是,list2 是一个 Arraylist,其类型是 Wanger 及其子类。

注意,“关键”来了!list2 并不允许通过 add(E e) 方法向其添加 Wanger 或者 Wangxiaoer 的对象,唯一例外的是 null

“那就奇了怪了,既然不让存放元素,那要 Arraylist<? extends Wanger> 这样的 list2 有什么用呢?”

虽然不能通过 add(E e) 方法往 list2 中添加元素,但可以给它赋值。

Arraylist<Wanger> list = new Arraylist<>(4);

Wanger wanger = new Wanger();
list.add(wanger);

Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);

Arraylist<? extends Wanger> list2 = list;

Wanger w2 = list2.get(1);
System.out.println(w2);

System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));

Arraylist<? extends Wanger> list2 = list; 语句把 list 的值赋予了 list2,此时 list2 == list。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 get()indexOf()contains()。想一想,如果可以向 list2 添加元素的话,这 3 个方法反而变得不太安全,它们的值可能就会变。

利用 <? super Wanger> 形式的通配符,可以向 Arraylist 中存入父类是 Wanger 的元素,来看例子。

Arraylist<? super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger());
list3.add(new Wangxiaoer());

// Wanger w3 = list3.get(0);

需要注意的是,无法从 Arraylist<? super Wanger> 这样类型的 list3 中取出数据。

List<? extends T>表示该集合中存在的都是类型T的子类,包括T自己。

而List<? super T>表示该集合中存的都是类型T的父类,包括T自己。

List<? extends T>如果去添加元素的时候,因为list中存放的其实是T的一种子类,如果我们去添加元素,其实不知道到底应该添加T的哪个子类,这个时候桥接方法在进行强转的时候会出错。但是如果是从集合中将元素取出来,我们可以知道取出来的元素肯定是T类型。所以? extends T这种方式可以取元素而不能添加,这个叫get原则。

List<? super T>因为存的都是类型T的父类,所以如果去添加T类或者T类子类的元素,肯定是可以的。但是如果将元素取出来,则不知道到底是什么类型,所以? super T可以添加元素但是没法取出来,这个叫put原则。

注解

注解(Annotation)是在 Java 1.5 时引入的概念,同 class 和 interface 一样,也属于一种类型。注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。

注解的生命周期

注解的生命周期有 3 种策略,定义在 RetentionPolicy 枚举中。

1)SOURCE:在源文件中有效,被编译器丢弃。

2)CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。

3)RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。

注解装饰的目标

注解的目标定义了注解将适用于哪一种级别的 Java 代码上,有些注解只适用于方法,有些只适用于成员变量,有些只适用于类,有些则都适用。截止到 Java 9,注解的类型一共有 11 种,定义在 ElementType 枚举中。

1)TYPE:用于类、接口、注解、枚举

2)FIELD:用于字段(类的成员变量),或者枚举常量

3)METHOD:用于方法

4)PARAMETER:用于普通方法或者构造方法的参数

5)CONSTRUCTOR:用于构造方法

6)LOCAL_VARIABLE:用于变量

7)ANNOTATION_TYPE:用于注解

8)PACKAGE:用于包

9)TYPE_PARAMETER:用于泛型参数

10)TYPE_USE:用于声明语句、泛型或者强制转换语句中的类型

11)MODULE:用于模块

手撸注解

撸一个字段注解吧,它用来标记对象在序列化成 JSON 的时候要不要包含这个字段。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}

1)JsonField 注解的生命周期是 RUNTIME,也就是运行时有效。

2)JsonField 注解装饰的目标是 FIELD,也就是针对字段的。

3)创建注解需要用到 @interface 关键字。

4)JsonField 注解有一个参数,名字为 value,类型为 String,默认值为一个空字符串。

为什么参数名要为 value 呢?有什么特殊的含义吗?

当然是有的,value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用 @JsonField(value = "沉默王二"),也可以把 value = 省略,变成 @JsonField("沉默王二")

使用JsonField注解

假设有一个 Writer 类,他有 3 个字段,分别是 age、name 和 bookName,后 2 个是必须序列化的字段。就可以这样来用 @JsonField 注解。

public class Writer {
    private int age;

    @JsonField("writerName")
    private String name;

    @JsonField
    private String bookName;

    public Writer(int age, String name, String bookName) {
        this.age = age;
        this.name = name;
        this.bookName = bookName;
    }

    // getter / setter

    @Override
    public String toString() {
        return "Writer{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", bookName='" + bookName + '\'' +
                '}';
    }
}

1)name 上的 @JsonField 注解提供了显式的字符串值。

2)bookName 上的 @JsonField 注解使用了缺省项。

接下来,我们来编写序列化类 JsonSerializer,内容如下:

public class JsonSerializer {
    public static String serialize(Object object) throws IllegalAccessException {
        Class<?> objectClass = object.getClass();
        Map<String, String> jsonElements = new HashMap<>();
        for (Field field : objectClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(JsonField.class)) {
                jsonElements.put(getSerializedKey(field), (String) field.get(object));
            }
        }
        return toJsonString(jsonElements);
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }

    private static String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet()
                .stream()
                .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
                .collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }
}

1)serialize() 方法是用来序列化对象的,它接收一个 Object 类型的参数。objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 isAnnotationPresent() 判断字段是否装饰了 JsonField 注解,如果是的话,调用 getSerializedKey() 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。

2)getSerializedKey() 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。

3)toJsonString() 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。

测试类 JsonFieldTest

public class JsonFieldTest {
    public static void main(String[] args) throws IllegalAccessException {
        Writer cmower = new Writer(18,"沉默王二","Web全栈开发进阶之路");
        System.out.println(JsonSerializer.serialize(cmower));
    }
}

结果:

{"bookName":"Web全栈开发进阶之路","writerName":"沉默王二"}

从结果上来看:

1)Writer 类的 age 字段没有装饰 @JsonField 注解,所以没有序列化。

2)Writer 类的 name 字段装饰了 @JsonField 注解,并且显示指定了字符串“writerName”,所以序列化后变成了 writerName。

3)Writer 类的 bookName 字段装饰了 @JsonField 注解,但没有显式指定值,所以序列化后仍然是 bookName。

枚举(enum)

枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。

创建一个枚举类型PlayerType

public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
}

虽然这里并没有看到继承关系,我们可以看一下反编译后的字节码

public final class PlayerType extends Enum
{

    public static PlayerType[] values()
    {
        return (PlayerType[])$VALUES.clone();
    }

    public static PlayerType valueOf(String name)
    {
        return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
    }

    private PlayerType(String s, int i)
    {
        super(s, i);
    }

    public static final PlayerType TENNIS;
    public static final PlayerType FOOTBALL;
    public static final PlayerType BASKETBALL;
    private static final PlayerType $VALUES[];

    static 
    {
        TENNIS = new PlayerType("TENNIS", 0);
        FOOTBALL = new PlayerType("FOOTBALL", 1);
        BASKETBALL = new PlayerType("BASKETBALL", 2);
        $VALUES = (new PlayerType[] {
            TENNIS, FOOTBALL, BASKETBALL
        });
    }
}

“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。”

  • 要继承 Enum 类;
  • 要写构造方法;
  • 要声明静态变量和数组;
  • 要用 static 块来初始化静态变量和数组;
  • 要提供静态方法,比如说 values()valueOf(String name)

既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用

eg:

public class Player {
    private PlayerType type;
    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }
    
    public boolean isBasketballPlayer() {
      return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
        return type;
    }

    public void setType(PlayerType type) {
        this.type = type;
    }
}

PlayerType 就相当于 Player 的内部类。

由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 isBasketballPlayer() 方法。

那为什么不使用 equals() 方法判断呢?

if(player.getType().equals(Player.PlayerType.BASKETBALL)){};

“==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException,而 equals() 方法则会。

另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 equals() 方法则不会

如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了

eg:

public enum PlayerType {
    TENNIS("网球"),
    FOOTBALL("足球"),
    BASKETBALL("篮球");

    private String name;

    PlayerType(String name) {
        this.name = name;
    }
}

EnumSet 是一个专门针对枚举类型的 Set 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效,从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。

因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。

想了解更多EnumSet

Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。

eg:

public class Singleton {  
    private volatile static Singleton singleton; 
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {
        synchronized (Singleton.class) { 
        if (singleton == null) {  
            singleton = new Singleton(); 
        }  
        }  
    }  
    return singleton;  
    }  
}

“要用到 volatile、synchronized 关键字等等,但枚举的出现,让代码量减少到极致。”

public enum EasySingleton{
    INSTANCE;
}

枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。

反射

什么是反射

要想知道什么是反射,就需要先来了解什么是‘正射’。一般情况下,我们在使用某个类之前已经确定它到底是个什么类了,拿到手就直接可以使用 new 关键字来调用构造方法进行初始化,之后使用这个类的对象来进行操作。

eg:

Writer writer = new Writer();
writer.setName("齐天大圣");

像上面这个例子,就可以理解为“正射”。而反射就意味着一开始我们不知道要初始化的类到底是什么,也就没法直接使用 new 关键字创建对象了。

我们只知道这个类的一些基本信息,就好像我们看电影的时候,为了抓住一个犯罪嫌疑人,警察就会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为反射

Class clazz = Class.forName("com.itwanger.s39.Writer");
Method method = clazz.getMethod("setName", String.class);
Constructor constructor = clazz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object,"齐天大圣");

像上面这个例子,就可以理解为“反射”。

是的,反射的成本是要比正射的高得多。

反射的缺点主要有两个。

  • 破坏封装: 由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。
  • 性能开销: 由于反射涉及到动态解析,因此无法执行 Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。

反射有哪些好处呢?

反射的主要应用场景有:

  • 开发通用框架:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
  • 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
  • 注解:注解本身只是起到一个标记符的作用,它需要利用发射机制,根据标记符去执行特定的行为。

eg: Writer 类,有两个字段,然后还有对应的 getter/setter。

public class Writer {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

测试类:

public class ReflectionDemo1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Writer writer = new Writer();
        writer.setName("齐天大圣");
        System.out.println(writer.getName());

        Class clazz = Class.forName("com.itwanger.s39.Writer");
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();

        Method setNameMethod = clazz.getMethod("setName", String.class);
        setNameMethod.invoke(object, "齐天大圣");
        Method getNameMethod = clazz.getMethod("getName");
        System.out.println(getNameMethod.invoke(object));
    }
}

来看一下输出结果:

齐天大圣
齐天大圣

只不过,反射的过程略显曲折了一些。

第一步,获取反射类的 Class 对象:

Class clazz = Class.forName("com.itwanger.s39.Writer");

第二步,通过 Class 对象获取构造方法 Constructor 对象:

Constructor constructor = clazz.getConstructor();

第三步,通过 Constructor 对象初始化反射类对象:

Object object = constructor.newInstance();

第四步,获取要调用的方法的 Method 对象:

Method setNameMethod = clazz.getMethod("setName", String.class);
Method getNameMethod = clazz.getMethod("getName");

第五步,通过 invoke() 方法执行:

setNameMethod.invoke(object, "齐天大圣");
getNameMethod.invoke(object)

经过这五个步骤,基本上就掌握了反射的使用方法。

掌握反射的基本使用方法确实不难,但要理解整个反射机制还是需要花一点时间去了解一下 Java 虚拟机的类加载机制的。

要想使用反射,首先需要获得反射类的 Class 对象,每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。

也就是说,java.lang.Class 是所有反射 API 的入口。

而方法的反射调用,最终是由 Method 对象的 invoke() 方法完成的,来看一下源码(JDK 8 环境下)。

@CallerSensitive
public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
        InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

两个嵌套的 if 语句是用来进行权限检查的。

MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类继承了这个抽象类。

  • NativeMethodAccessorImpl:通过本地方法来实现反射调用;
  • DelegatingMethodAccessorImpl:通过委派模式来实现反射调用;

通过 debug 的方式进入 invoke() 方法后,可以看到第一次反射调用会生成一个委派实现 DelegatingMethodAccessorImpl,它在生成的时候会传递一个本地实现 NativeMethodAccessorImpl。

也就是说,invoke() 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法。

“为什么不直接调用本地实现呢?”三妹问。

“之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。”

那临界点是多少呢?

默认是 15 次

1) 获取反射类的Class对象

Class.forName(), 参数为反射类的完全限定名

Class c1 = Class.forName("com.itwanger.s39.ReflectionDemo3");
System.out.println(c1.getCanonicalName());

Class c2 = Class.forName("[D");
System.out.println(c2.getCanonicalName());

Class c3 = Class.forName("[[Ljava.lang.String;");
System.out.println(c3.getCanonicalName());

来看一下输出结果:

com.itwanger.s39.ReflectionDemo3
double[]
java.lang.String[][]

类名 + .class,只适合在编译前就知道操作的 Class

Class c1 = ReflectionDemo3.class;
System.out.println(c1.getCanonicalName());

Class c2 = String.class;
System.out.println(c2.getCanonicalName());

Class c3 = int[][][].class;
System.out.println(c3.getCanonicalName());

来看一下输出结果:

com.itwanger.s39.ReflectionDemo3
java.lang.String
int[][][]

2)创建反射类的对象

通过反射来创建对象的方式有两种:

  • 用 Class 对象的 newInstance() 方法。
  • 用 Constructor 对象的 newInstance() 方法。
Class c1 = Writer.class;
Writer writer = (Writer) c1.newInstance();

Class c2 = Class.forName("com.itwanger.s39.Writer");
Constructor constructor = c2.getConstructor();
Object object = constructor.newInstance();

3)获取构造方法

Class 对象提供了以下方法来获取构造方法 Constructor 对象:

  • getConstructor():返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
  • getDeclaredConstructor():返回反射类的特定构造方法,不限定于 public 的。
  • getConstructors():返回类的所有 public 构造方法。
  • getDeclaredConstructors():返回类的所有构造方法,不限定于 public 的。
Class c2 = Class.forName("com.itwanger.s39.Writer");
Constructor constructor = c2.getConstructor();

Constructor[] constructors1 = String.class.getDeclaredConstructors();
for (Constructor c : constructors1) {
    System.out.println(c);
}

4)获取字段

大体上和获取构造方法类似,把关键字 Constructor 换成 Field 即可。

Method setNameMethod = clazz.getMethod("setName", String.class);
Method getNameMethod = clazz.getMethod("getName");

5)获取方法

大体上和获取构造方法类似,把关键字 Constructor 换成 Method 即可。

Method[] methods1 = System.class.getDeclaredMethods();
Method[] methods2 = System.class.getMethods();

注意,如果你想反射访问私有字段和(构造)方法的话,需要使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。

反射学习参考:

第一篇:大白话说Java反射:入门、使用、原理

字符串&数组

字符串为什么是不可变的

  • String 类被 final 关键字修饰,所以它不会有子类,这就意味着没有子类可以重写它的方法,改变它的行为。
  • String 类的数据存储在 byte[] 数组中,而这个数组也被 final 关键字修饰了,这就表示 String 对象是没法被修改的,只要初始化一次,值就确定了。

为什么要这样设计呢?

第一,可以保证 String 对象的安全性,避免被篡改,毕竟像密码这种隐私信息一般就是用字符串存储的。

第二,保证哈希值不会频繁变更。毕竟要经常作为哈希表的键值,经常变更的话,哈希表的性能就会很差劲。

第三,可以实现字符串常量池。

字符串常量池

先从这道面试题开始吧!

String s = new String("菜鸟");

使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘菜鸟’这个字符串对象,如果有,就不会在字符串常量池中创建‘菜鸟’这个对象了,直接在堆中创建一个‘菜鸟’的字符串对象,然后将堆中这个‘菜鸟’的对象地址返回赋值给变量 s。

如果没有,先在字符串常量池中创建一个‘菜鸟’的字符串对象,然后再在堆中创建一个‘菜鸟’的字符串对象,然后将堆中这个‘菜鸟’的字符串对象地址返回赋值给变量 s。

由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一个字符串常量池。

通常情况下,我们会采用双引号的方式来创建字符串对象,而不是通过 new 关键字的方式:

String s = "三妹";

当执行 String s = "三妹" 时,Java 虚拟机会先在字符串常量池中查找有没有“三妹”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“三妹”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“三妹”这个对象,然后将其地址返回,赋给变量 s。

字符串常量池在内存中的什么位置呢?

在 Java 8 之前,字符串常量池在永久代中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aVldCJI5-1658832846992)(C:\Users\26308\Desktop\学习记录\java学习\java语言\images\JVM内存模型Java8之前.png)]

Java 8 之后,移除了永久代,字符串常量池就移到了堆中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZkqTu3j-1658832846993)(C:\Users\26308\Desktop\学习记录\java学习\java语言\images\JVM内存模型Java8之后.png)]

方法区,永久代和元空间的概念

  • 方法区是 Java 虚拟机规范中的一个概念,就像是一个接口吧;
  • 永久代是 HotSpot 虚拟机中对方法的一个实现,就像是接口的实现类;
  • Java 8 的时候,移除了永久代,取而代之的是元空间,是方法区的另外一个实现。

String&intern

针对没有使用双引号声明的字符串对象来说,就像下面代码中的 s1 那样:

String s1 = new String("菜鸟") + new String("学Java");

如果想把 s1 的内容也放入字符串常量池的话,可以调用 intern() 方法来完成。

不过,需要注意的是,Java 7 的时候,字符串常量池从永久代中移动到了堆中,虽然此时永久代还没有完全被移除。Java 8 的时候,永久代被彻底移除。

这个变化也直接影响了 String.intern() 方法在执行时的策略,Java 7 之前,执行 String.intern() 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象; Java 7 之后呢,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。

eg:猜猜这段代码输出的结果吧

String s1 = new String("齐天大圣");
String s2 = s1.intern();
System.out.println(s1 == s2);

第一行代码,字符串常量池中会先创建一个“齐天大圣”的对象,然后堆中会再创建一个“二哥三妹”的对象,s1 引用的是堆中的对象。

第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找“二哥三妹”这个字符串是否存在,此时是存在的,所以 s2 引用的是字符串常量池中的对象。

也就意味着 s1 和 s2 的引用地址是不同的,一个来自堆,一个来自字符串常量池,所以输出的结果为 false。

false

我们再来看下面这段代码。

String s1 = new String("孙悟空") + new String("猪八戒");
String s2 = s1.intern();
System.out.println(s1 == s2);

第一行代码,会在字符串常量池中创建两个对象,一个是“孙悟空”,一个是“猪八戒”,然后在堆中会创建两个匿名对象“孙悟空”和“猪八戒”(可以暂时忽略),最后还有一个“二哥三妹”的对象,s1 引用的是堆中“二哥三妹”这个对象。

第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找“二哥三妹”这个对象是否存在,此时不存在的,但堆中已经存在了,所以字符串常量池中保存的是堆中这个“孙悟空猪八戒”对象的引用,也就是说,s2 和 s1 的引用地址是相同的,所以输出的结果为 true。

true

不过需要注意的是,尽管 intern 可以确保所有具有相同内容的字符串共享相同的内存空间,但也不要烂用 intern,因为任何的缓存池都是有大小限制的,不能无缘无故就占用了相对稀缺的缓存空间,导致其他字符串没有坑位可占。

另外,字符串常量池本质上是一个固定大小的 StringTable,如果放进去的字符串过多,就会造成严重的哈希冲突,从而导致链表变长,链表变长也就意味着字符串常量池的性能会大幅下降,因为要一个一个找是需要花费时间的。

字符串的比较

  • “==”操作符用于比较两个对象的地址是否相等。
  • .equals() 方法用于比较两个对象的内容是否相等。
"小萝莉" == "小" + "萝莉"

“由于‘小’和‘萝莉’都在字符串常量池,所以编译器在遇到‘+’操作符的时候将其自动优化为“小萝莉”,所以返回 true。”

new String("小萝莉").intern() == "小萝莉"

new String("小萝莉") 在执行的时候,会先在字符串常量池中创建对象,然后再在堆中创建对象;执行 intern() 方法的时候发现字符串常量池中已经有了‘小萝莉’这个对象,所以就直接返回字符串常量池中的对象引用了,那再与字符串常量池中的‘小萝莉’比较,当然会返回 true 了。”

1)Objects.equals()

Objects.equals() 这个静态方法的优势在于不需要在调用之前判空。

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

如果直接使用 a.equals(b),则需要在调用之前对 a 进行判空,否则可能会抛出空指针 java.lang.NullPointerExceptionObjects.equals() 用起来就完全没有这个担心。

Objects.equals("小萝莉", new String("小" + "萝莉")) // --> true
Objects.equals(null, new String("小" + "萝莉")); // --> false
Objects.equals(null, null) // --> true

String a = null;
a.equals(new String("小" + "萝莉")); // throw exception

2)String 类的 .contentEquals()

.contentEquals() 的优势在于可以将字符串与任何的字符序列(StringBuffer、StringBuilder、String、CharSequence)进行比较。

public boolean contentEquals(CharSequence cs) {
    // Argument is a StringBuffer, StringBuilder
    if (cs instanceof AbstractStringBuilder) {
        if (cs instanceof StringBuffer) {
            synchronized(cs) {
                return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        } else {
            return nonSyncContentEquals((AbstractStringBuilder)cs);
        }
    }
    // Argument is a String
    if (cs instanceof String) {
        return equals(cs);
    }
    // Argument is a generic CharSequence
    int n = cs.length();
    if (n != length()) {
        return false;
    }
    byte[] val = this.value;
    if (isLatin1()) {
        for (int i = 0; i < n; i++) {
            if ((val[i] & 0xff) != cs.charAt(i)) {
                return false;
            }
        }
    } else {
        if (!StringUTF16.contentEquals(val, cs, n)) {
            return false;
        }
    }
    return true;
}

字符串拼接

字符串分割

“这是建立在字符串是确定的情况下,最重要的是分隔符是确定的。否则,麻烦就来了。”我说,“大约有 12 种英文特殊符号,如果直接拿这些特殊符号替换上面代码中的分隔符(中文逗号),这段程序在运行的时候就会出现以下提到的错误。”

  • 反斜杠 \(ArrayIndexOutOfBoundsException)
  • 插入符号 ^(同上)
  • 美元符号 $(同上)
  • 逗点 .(同上)
  • 竖线 |(正常,没有出错)
  • 问号 ?(PatternSyntaxException)
  • 星号 *(同上)
  • 加号 +(同上)
  • 左小括号或者右小括号 ()(同上)
  • 左方括号或者右方括号 [](同上)
  • 左大括号或者右大括号 {}(同上)

数组

数组的声明方式分两种。

先来看第一种:

int[] anArray;

再来看第二种:

int anOtherArray[];

不同之处就在于中括号的位置,是跟在类型关键字的后面,还是跟在变量的名称的后面。前一种的使用频率更高一些,像 ArrayList 的源码中就用了第一种方式。

同样的,数组的初始化方式也有多种,最常见的是:

int[] anArray = new int[10];

看到了没?上面这行代码中使用了 new 关键字,这就意味着数组的确是一个对象,只有对象的创建才会用到 new 关键字,基本数据类型是不用的。然后,我们需要在方括号中指定数组的长度。

这时候,数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同

另外,还可以使用大括号的方式,直接初始化数组中的元素:

int anOtherArray[] = new int[] {1, 2, 3, 4, 5};

遍历数组:

第一种,使用 for 循环:

int anOtherArray[] = new int[] {1, 2, 3, 4, 5};
for (int i = 0; i < anOtherArray.length; i++) {
    System.out.println(anOtherArray[i]);
}

通过 length 属性获取到数组的长度,然后从 0 开始遍历,就得到了数组的所有元素。

第二种,使用 for-each 循环:

for (int element : anOtherArray) {
    System.out.println(element);
}

如果不需要关心索引的话(意味着不需要修改数组的某个元素),使用 for-each 遍历更简洁一些。当然,也可以使用 while 和 do-while 循环。

在 Java 中,可变参数用于将任意数量的参数传递给方法,来看 varargsMethod() 方法:

void varargsMethod(String... varargs) {}

数组转List

最原始的方式,就是通过遍历数组的方式,一个个将数组添加到 List 中。

int[] anArray = new int[] {1, 2, 3, 4, 5};

List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
    aList.add(element);
}

更优雅的方式是通过 Arrays 类的 asList() 方法:

List<Integer> aList = Arrays.asList(anArray);

但需要注意的是,该方法返回的 ArrayList 并不是 java.util.ArrayList,它其实是 Arrays 类的一个内部类:

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable{}

如果需要添加元素或者删除元素的话,需要把它转成 java.util.ArrayList

new ArrayList<>(Arrays.asList(anArray));

Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。

String[] anArray = new String[] {"Java", "编程", "一门有趣的编程语言"};
Stream<String> aStream = Arrays.stream(anArray);

如果想对数组进行排序的话,可以使用 Arrays 类提k供的 sort() 方法。

  • 基本数据类型按照升序排列
  • 实现了 Comparable 接口的对象按照 compareTo() 的排序

来看第一个例子:

int[] anArray = new int[] {5, 2, 1, 4, 8};
Arrays.sort(anArray);

排序后的结果如下所示:

[1, 2, 4, 5, 8]

来看第二个例子:

String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"};
Arrays.sort(yetAnotherArray, 1, 3,
                Comparator.comparing(String::toString).reversed());

只对 1-3 位置上的元素进行反序,所以结果如下所示:

[A, Z, E, B, C]

有时候,我们需要从数组中查找某个具体的元素,最直接的方式就是通过遍历的方式:f

int[] anArray = new int[] {5, 2, 1, 4, 8};
for (int i = 0; i < anArray.length; i++) {
    if (anArray[i] == 4) {
        System.out.println("找到了 " + i);
        break;
    }
}

上例中从数组中查询元素 4,找到后通过 break 关键字退出循环。

如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。Arrays.binarySearch() 方法可供我们使用,它需要传递一个数组,和要查找的元素。

int[] anArray = new int[] {1, 2, 3, 4, 5};
int index = Arrays.binarySearch(anArray, 4);

打印数组

数组也是一个对象,但 Java 中并未明确的定义这样一个类。因此数组也就没有机会覆盖 Object.toString() 方法。如果尝试直接打印数组的话,输出的结果并不是我们预期的结果。

来看这样一个例子。

String [] cmowers = {"Java","编程","一门有趣的编程语言"};
System.out.println(cmowers);

程序打印的结果是:

[Ljava.lang.String;@3d075dc0

[Ljava.lang.String; 表示字符串数组的 Class 名,@ 后面的是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 java.lang.Object 类的 toString() 方法就明白了。

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

我们来看第一种打印数组的方法,使用时髦一点的 Stream 流。

第一种形式:

Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s));

第二种形式:

Stream.of(cmowers).forEach(System.out::println);

第三种形式:

Arrays.stream(cmowers).forEach(System.out::println);

打印的结果如下所示。

Java
编程
一门有趣的编程语言

当然了,也可以使用比较土的方式,for 循环。甚至 for-each 也行。

for(int i = 0; i < cmowers.length; i++){
    System.out.println(cmowers[i]);
}

for (String s : cmowers) {
    System.out.println(s);
}

Arrays.toString() 是打印数组的最佳方式

String [] cmowers = {"Java","编程","一门有趣的编程语言"};
System.out.println(Arrays.toString(cmowers));

程序打印结果:

[Java, 编程,一门有趣的编程语言]

倘若想打印二维数组呢?

可以使用 Arrays.deepToString() 方法。

String[][] deepArray = new String[][] {{"Java", "编程"}, {"一门有趣的编程语言"}};
System.out.println(Arrays.deepToString(deepArray));
[[Java, 编程], [一门有趣的编程语言]]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值