Java基础

2017/07/01 Java 共 10349 字,约 30 分钟

Java基础

JDK, JRE, JVM

JDK(Java Development Kit)是一个功能齐全的 Java 开发工具包,供开发者使用,用于创建和编译 Java 程序。它包含了 JRE(Java Runtime Environment),以及编译器 javac 和其他工具,如 javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)等。

JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分:

  1. JVM : 也就是我们上面提到的 Java 虚拟机。
  2. Java 基础类库(Class Library):一组标准的类库,提供常用的功能和 API(如 I/O 操作、网络通信、数据结构等)。

简单来说,JRE 只包含运行 Java 程序所需的环境和类库,而 JDK 不仅包含 JRE,还包括用于开发和调试 Java 程序的工具。

jdk-include-jre

不过,从 JDK 9 开始,就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ jlink 工具 (随 Java 9 一起发布的新命令行工具,用于生成自定义 Java 运行时映像,该映像仅包含给定应用程序所需的模块) 。并且,从 JDK 11 开始,Oracle 不再提供单独的 JRE 下载。

JVM

Java 虚拟机(Java Virtual Machine, JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

如下图所示,不同编程语言(Java、Groovy、Kotlin、JRuby、Clojure …)通过各自的编译器编译成 .class 文件,并最终通过 JVM 在不同平台(Windows、Mac、Linux)上运行。

运行在 Java 虚拟机之上的编程语言

JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。

除了我们平时最常用的 HotSpot VM 外,还有 J9 VM、Zing VM、JRockit VM 等 JVM 。维基百科上就有常见 JVM 的对比:Comparison of Java virtual machines

编译运行

  • Java 编译只需使用 javac 命令,如 javac Hello.java。它只编译,不执行。
  • 可以一次性编译多个文件:javac *.java。
  • 编译带包名的类建议使用 -d 参数指定输出路径,如 javac -d out src/com/example/Main.java。
  • 编译后的 .class 文件需要用 java 命令运行。 每个类包括内部类,非public类都会单独一个class文件
  • 运行时不能带 .class 后缀,必须使用完全限定类名(包括包名)。有包结构就只能用包名。
  • 示例:如果 Main.java 包含 package org.example;,应先进入 org 的上级目录,再执行 java org.example.Main。
    • 错误示例:直接用 java Main 会报 wrong name: Main,因为要用包名

注释

文档注释可用在三个地方,类、字段和方法,用来解释它们是干嘛的。

/**
 * 微信搜索「沉默王二」,回复 Java
 */
public class Demo {
    /**
     * 姓名
     */
    private int age;

    /**
     * main 方法作为程序的入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {

    }
}

1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。

2)所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。

3)所有的类都必须添加创建者和创建日期。

4)所有的枚举类型字段必须要有注释,说明每个数据项的用途。

5)代码修改的同时,注释也要进行相应的修改。

单行注释通常用于解释方法内某单行代码的作用。 但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的。 正确的单行注释如上图中所说,在被注释语句上方另起一行,使用 // 注释。

一、数据类型

基本类型

  • byte/8
  • char/16
  • short/16
  • int/32
  • float/32
  • long/64
  • double/64
  • boolean/~

boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

  • 对于单独使用的 boolean 类型,JVM 并没有提供专用的字节码指令,而是使用 int 相关的指令 istore 来处理,那么 int 明确是 4 个字节,所以此时的 boolean 也占用 4 个字节。
  • 对于作为数组来使用的 boolean 类型,JVM 会按照 byte 的指令来处理(bastore),那么已知 byte 类型占用 1 个字节,所以此时的 boolean 也占用 1 个字节。

包装类型

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x;         // 拆箱 调用了 X.intValue()

引用类型

基本数据类型:

  • 1、变量名指向具体的数值。
  • 2、基本数据类型存储在栈上。

引用数据类型:

  • 1、变量名指向的是存储对象的内存地址,在栈上。
  • 2、内存地址指向的对象存储在堆上。

除了基本数据类型以外的类型,都是所谓的引用类型。常见的有数组(对,没错,数组是引用类型,后面我们会讲)、class(也就是类),以及接口(指向的是实现接口的类的对象)。

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

二、String

概览

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

在 Java 8 中,String 内部使用 char 数组存储数据。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

不可变的好处

1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

String, StringBuffer and StringBuilder

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

new String(“abc”)

new String("abc") 会创建一个堆上的新对象,即使 "abc" 已经存在于字符串常量池中。

是否创建新对象?

  • 是,始终会在堆上创建一个新对象;
  • 如果字面量 "abc" 不在常量池,还会额外创建一个常量池对象。是两个分开的对象,只不过内容相同。

和直接赋值有何不同?

String s1 = "abc";               // s1 指向常量池对象
String s2 = new String("abc");   // s2 指向堆对象
System.out.println(s1 == s2);    // false

常见面试陷阱题:

String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // false,不同堆对象

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。

  • “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。 是两个分开的对象,只不过内容相同。

第一,使用双引号声明的字符串对象会保存在字符串常量池中。

第二,使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。

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

String s1 = new String("二哥") + new String("三妹");

s1在堆里,其他在堆和string pool都有一份。

当编译器遇到 + 号这个操作符的时候,会将 new String("二哥") + new String("三妹") 这行代码编译为以下代码:

new StringBuilder().append("二哥").append("三妹").toString();

String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

intern() 方法会:

  • 如果常量池中存在相同内容的字符串,返回池中对象的引用;
  • 否则,把当前字符串的引用加入常量池,并返回这个引用。
String s1 = new String("二哥三妹");
String s2 = s1.intern();
System.out.println(s1 == s2); // false。因为s2是string pool的,s1是堆的。

String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4);           // true


String s1 = new String("二哥") + new String("三妹");
String s2 = s1.intern();
System.out.println(s1 == s2); // true. 此时string pool里没有s1,s1只有在堆里有。intern()时添加s1到string pool里,并返回引用。引用的是堆里的s1.

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

String s5 = "bbb"; 
String s6 = "bbb";
System.out.println(s5 == s6);  // true
  • JDK 6 及以前:intern 的字符串存在永久代(PermGen);

  • JDK 7+:intern 的字符串存在堆中,性能更好。

  • String Pool相同内容只有一份,堆可以多份一样的。
  • String Pool可以理解为是堆的一部分。 String Pool 在 堆(Heap) 中,会动态扩容,不会直接崩溃。采用哈希表存储字符串,满了之后会进行 rehash(重新分配更大的哈希表)。避免对随机生成的大量字符串调用 intern(),因为这会使 String Pool 变得过大并增加 GC 压力。

String.format

String类有个格式化方法format(),返回一个String对象

String fs;
fs=String.format("%d is 3",3);

String format()方法的语法为:

String.format(String format, Object... args)

格式说明符

以下是常用的格式说明符:

说明符描述
%b, %B根据参数为“ true”或“ false”
%s, %S一个字符串, 也可以是其他,万能匹配
%c, %CUnicode字符
%d十进制整数(仅用于整数)
%f用于十进制数字(用于浮点数)

不知道用啥就用%s

十进制数的格式

示例

class Main {
  public static void main(String[] args) {

    float n1 = -452.534f;
    double n2 = -345.766d;

    //按原样格式化浮点数
    System.out.println(String.format("n1 = %f", n1)); // -452.533997
    System.out.println(String.format("n2 = %f", n2)); // -345.766000

    //显示到小数点后两位
    System.out.println(String.format("n1 = %.2f", n1)); // -452.53
    System.out.println(String.format("n2 = %.2f", n2)); // -345.77
  }
}

三、运算

参数传递

Java 的参数是以值传递的形式传入方法中,而不是引用传递。

对于对象,传递的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。

float 与 double

Java 不能隐式执行向下转型,因为这会使得精度降低。

1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。

// float f = 1.1;

1.1f 字面量才是 float 类型。

float f = 1.1f;

隐式类型转换

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型。

short s1 = 1;
// s1 = s1 + 1;

但是使用 += 或者 ++ 运算符会执行隐式类型转换。

s1 += 1;
s1++;

上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:

s1 = (short) (s1 + 1);

switch

从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

String s = "a";
switch (s) {
    case "a":
        System.out.println("aaa");
        break;
    case "b":
        System.out.println("bbb");
        break;
}

switch 不支持 long、float、double,是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

四、关键字

final

1. 数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;

2. 方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

3. 类

声明类不允许被继承。

static

父类中的静态成员变量和方法是可以被子类继承的,但是不能被自己重写,无法形成多态

1. 静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {

    private int x;         // 实例变量
    private static int y;  // 静态变量

    public static void main(String[] args) {
        // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
        A a = new A();
        int x = a.x;
        int y = A.y;
    }
}

2. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
}

只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因为这两个关键字与具体对象关联。

public class A {

    private static int x;
    private int y;

    public static void func1(){
        int a = x;
        // int b = y;  // Non-static field 'y' cannot be referenced from a static context
        // int b = this.y;     // 'A.this' cannot be referenced from a static context
    }
}

1、父类方法如果是静态方法,子类不能覆盖为非静态方法;

2、父类方法如果是非静态方法,子类不能覆盖为静态方法;

3、父类静态方法可以被覆盖,允许在子类中定义同名的静态方法,但是没有多态。

3. 静态语句块

静态语句块在类初始化时运行一次。

public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
123

4. 静态内部类

非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。

public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

静态内部类不能访问外部类的非静态的变量和方法。

5. 静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

6. 初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

public static String staticField = "静态变量";
static {
    System.out.println("静态语句块");
}
public String field = "实例变量";
{
    System.out.println("普通语句块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
    System.out.println("构造函数");
}

变量区别

成员变量:

在类中,方法外; 有默认初始值;通过对象调用,存储在堆中;与对象共存亡,也叫实例变量

局部变量:

在方法中,或者方法的形式参数;没有默认初始值;存储在栈中;与方法共存亡

静态变量:

在类中,方法外;有默认初始值;通过对象调用,类名调用;存储在静态存储区;与类共存亡,也叫做类变量

初始化顺序

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块: 之间按声明顺序先后初始化)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块: 之间按声明顺序先后初始化)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

数组

声明和创建

声明数组变量

dataType[] arrayRefVar;   // 首选的方法

或

dataType arrayRefVar[];  // 效果相同,但不是首选方法

创建数组

arrayRefVar = new dataType[arraySize];
//OR
dataType[] arrayRefVar = {value0, value1, ..., valuek};

数组作为函数的参数

public static void printArray(int[] array)

数组作为函数的返回值

public static int[] reverse(int[] list)

多维数组

多维数组的动态初始化(以二维数组为例)

  1. 直接为每一维分配空间
    type arrayName = new type[arraylenght1][arraylenght2];

    int a[][] = new int[2][3];
  1. 从最高维开始,分别为每一维分配空间,例如:
    String s[][] = new String[2][];
    s[0] = new String[2];
    s[1] = new String[3];
    s[0][0] = new String("Good");
    s[0][1] = new String("Luck");
    s[1][0] = new String("to");
    s[1][1] = new String("you");
    s[1][2] = new String("!");

Arrays 类

java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。

具有以下功能:

给数组赋值:通过 fill 方法。
对数组排序:通过 sort 方法,按升序。
比较数组:通过 equals 方法比较数组中元素值是否相等。
查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。

队列函数记忆法

非阻塞访问就用

  • offer, poll, peak。他们不会抛异常。用在阻塞队列上就是非阻塞访问,尝试读写,没有就是null。 用在非阻塞队列就是和正常一样

  • 对BlockQueue有点特殊,是提供了超时的阻塞读写,因为它不支持非阻塞的访问。

阻塞方案就用

  • put, take, 是阻塞读写。

抛出异常的读写

  • add, remove, get。

Search

    Table of Contents