跳至主要內容

Java基础 - 知识点

Zenghr大约 19 分钟Java

Java基础 - 基础知识

提示

本文对 Java 基础知识做一个总结,如有总结有错误请提出问题,我好改进。

Java简介

什么是编程

开始学习Java语言之前,我们先了解一下 计算机编程 是什么以及 Java 语言

计算机语言是为了实现人与计算机通讯的,为了让计算机实现现实人为比较复杂的工作。

我们把解决问题的步骤告诉计算机一步一步去做,把这个过程叫做编程。

Java语言

Java诞生于1995年,原属于SUN公司,2009年4月20日,美国数据软件巨头甲骨文公司(Oracle
宣布以74亿美元收购SUN公司

跨平台原理

Window 系统只能执行 Window 程序(exe),Linux 系统只能执行 Linux 系统程序,Mac 系统只能执行 Mac程序,所以一个程序想要在多个平台执行,就有了如下两个跨平台方式

1、多次编译,到处运行

提示

每个系统都有一个编译器,使用特定的编译器所编译的程序只能在对应的平台运行,所以每个平台编译一次就实现多次编译,到处运行

2、一次编译,到处运行

提示

Java 所实现的跨平台方式就是,一次编译,到处运行,实现原理:Java语言对 Java程序进行了编译操作,编译后生产和平台无关的 - 字节码文件。这个文件 Window、Linux、Mac等系统是不能直接运行的,该文件只能被 JVM 识别运行,JVM 是Java的虚拟机,所以我们想要在哪个系统上运行 Java 程序,就要在哪个系统上安装相应的 Java虚拟机,由虚拟机执行 Java程序,这就是实现了 Java 的跨平台

总结Java 跨平台原因:

  • Java文件经过编译后生成和平台无关的 class 文件
  • Java虚拟机是不跨平台

JDK

  • JDK(Java Development Kit): Java开发工具,包含开发 Java 程序的所有工具,如:javajavac等,JDK 包含 JRE,所以安装了 JDK 就不用安装 JRE了
  • JRE(Java Runtime Environment): Java 运行环境,JRE 包含 Java常用的类库以及JVM,一般在只运行程序不需要开发程序的服务器中安装
  • JVM(Java Virtual Machine): Java虚拟机,它是运行所有Java程序的虚拟计算机,JVM不夸平台。

数据类型

八大基本类型:

  • boolean/1(bit)
  • byte/8
  • char/16
  • short/16
  • int/32
  • long/64
  • float/32
  • double/64

基本数据类型转换

  • 自动类型转换: 隐式类型转换,范围小的数据类型 直接转换为 范围大的数据类型
  • 强制类型转换: 显示类型转换,范围大的数据类型 直接转换为 范围小的数据类型
  • 自动类型提升: 在表达式中,最终的结果类型,将会转换为表达式中类型最高的类型

包装类

提示

包装类就是把基本数据类型封装到一个类中,提供便利的方法,更好的操作基本类型

Integer包装类

Integer 内部封装了一个 int 类型的基本数据类型 value,并提供方法对 int 值进行操作,还提供 Stringint 之间的转换

装箱和拆箱概念

  • 装箱:将基本数据类型转成包装类的过程
  • 拆箱:将包装类转成基本数据类型的过程

自动装箱和自动拆箱

  • 自动装箱:将基本数据类型直接赋值给包装类的过程
  • 自动拆箱:将包装类直接赋值给基本数据类型的过程

缓存池

// 缓存池分析
Integer i5 = new Integer(20);
Integer i6 = new Integer(20);
System.out.println(i5 == i6); // false

Integer i7 = Integer.valueOf(30);
Integer i8 = 30;
System.out.println(i7 == i8); // true

Integer i9 = Integer.valueOf(400);
Integer i10 = 400;
System.out.println(i9 == i10); // false

new Integer(20)Integer.valueOf(20) 的区别:

  • new Integer(20) 每次都会新建一个对象

  • Integer.valueOf(20) 会使用缓存中的对象,多次调用会引用同一对象

valueOf 方法的实现比较简单,先判断值是否在缓存中,存在就使用缓存中的值,不存在就直接 new 新建一个对象

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

Integer 缓存池中默认大小为 【-128,127】

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

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

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

  • 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

在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。

运算符

& 和 && 的区别

&: 与符号,无论 & 左边表达式是否为 真, & 右边都参与运算

&&: 短路与,如果 && 左边表达式为 真,那么 && 右边不参与运算

JVM内存模型

JVM 内存结构大概分为:

方法区(Method Area)

  • 介绍:线程共享的内存区域,用于存储 类信息、常量、静态变量等
  • 控制参数:-XX:PermSize 设置堆的最小空间。-XX:MaxPermSize 设置堆的最大空间
  • 异常:当方法区无法满足内存分配需求时,将会抛出 OutOfMemoryError 异常

堆(Heap)

  • 介绍:线程共享的内存区域,所有的对象实例以及数组都要在堆中分配空间,堆由 GC 负责回收管理
  • 控制参数:-Xms 设置堆的最小空间。-Xmx 设置堆的最大空间
  • 异常:如果在堆中没有足够的空间完成实例分配,将会抛出 OutOfMemoryError 异常

虚拟机栈(JVM Stack)

  • 介绍:线程私有的内存区域,也称为 方法栈区,当方法执行时,就会创建一个当前方法的 栈帧(Stack Frame),用于存储 局部变量、操作数栈、动态链接等信息。每个方法执行结束时,就会清除该栈帧,方法调用执行至结束的过程,称之为栈帧的入栈到出栈过程。
  • 控制参数:-Xss 控制每个线程栈的大小
  • 异常:线程请求的栈深度大于虚拟机所允许的深度时抛出 StackOverflowError。 虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError

本地方法栈(Native Method Stack)

  • 介绍:和虚拟机栈作用相似
  • 区别:本地方法栈是为虚拟机使用 本地方法 服务的

程序计数器

  • 介绍:当前线程所执行的行号指示器

数组类型

数组的定义

提示

用来表示一组相同类型的变量,数组是 引用类型,访问元素 索引超出范围会引发 越界异常错误

静态初始化

定义数组的语法:数据类型[] 变量名 = new 数据类型[]{数据1, 数据二};

// 定义 int 类型的数组
int[] array = new int[]{1,2,3};

操作数组的方法:操作数组元素都是使用 索引 来操作,第一个索引都是 0 开始

// 访问数组元素
array[0]; // 1
array[1]; // 2
// 修改数组元素
array[0] = 6;
// 获取数组长度使用 length
array.length; // 3

语法糖初始化:

// 定义 int 类型的数组
int[] array = {1,2,3};

动态初始化

动态定义数组的语法:数据类型[] 变量名 = new 数据类型[数组长度];

// 定义 int 类型的数组
int[] array = new int[3];
// 动态设置元素
array[0] = 1;

数组初始化内存图分析

静态初始化内存分析:

​ 先在 栈空间(Stack) 创建 array 空间,用于存储 引用数组的 内存地址new int[]{1,2,3} new 关键字 在 堆空间(Heap) 分配空间 ,将空间的地址赋值给 array 变量,分析图如下:

mark

数组长度无法改变:

int[] array;
array = new int[]{1,2,3};
System.out.println(array.length); // 3
array = new int[]{1,2,3,4};
System.out.println(array.length); // 4

如下所示,array 只是储存的内存地址改变了,数组本身的长度怕没有改变,指向的是新的数组
mark

String类型的数组

String[] array = {"A", "B", "C"};
array[0] = "D";
// todo:并没有改变"A"本身,只是 array[0] 指向了 "D" 所在的内存地址

数组特点

  • 数组必须初始化才能使用,并且索引下标不能超过,否则会报错
  • 数组所有元素初始化为默认值,整型为 0,浮点类型是 0.0,布尔型是 false
  • 数组一旦创建长度无法改变
  • 对于储存的是引用类型时,改变元素时怕没有改变原来的数据,只是改变元素引用的值

Object 常用方法

提示

Object 类 是在 java.lang 包中,所有类都是继承于 Object,称之为 超类

方法摘要

public class Object {
    // 创建并返回此对象的一个副本。
    protected native Object clone() throws CloneNotSupportedException;
    // 指示其他某个对象是否与此对象“相等”。
    public boolean equals(Object obj);
    // 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
    protected void finalize() throws Throwable;
    // 返回此 Object 的运行时类。
    public final Class<?> getClass();
    // 返回该对象的哈希码值
    public int hashCode();
    // 返回该对象的字符串表示
    public String toString();
    // 唤醒在此对象监视器上等待的单个线程
    public final void notify();
    // 唤醒在此对象监视器上等待的所有线程
    public final void notifyAll();
    // 导致当前线程等待
    public final void wait() throws InterruptedException;
    
}

toString()

默认返回格式为:Cat@4554617c , @ 前面的是该类运行时的类,后面的是该对象的 哈希码。

当我们输出打印类的实例对象时,默认会调用 toString() 方法输出

源码分析

// getClass() 是 Object 的方法,返回运行时类
// hashCode() 是 Object 的方法,返回对象的哈希码值
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

演示

public class Cat {
    String name;
    
    public Cat() {}

    public Cat(String name) {
        this.name = name;
    }
}
public static void main(String[] args) {
    Cat cat = new Cat("招财猫");
    System.out.println(cat); // 
}

equals()

equals 用于比较其他某个对象是否与此对象“相等”。

源码分析

public boolean equals(Object obj) {
    return (this == obj);
}

Object 实现 equals 的方式是用 == 比较符,比较两个对象的内存地址是否一致或者是 哈希码值 是否一致

equals 与 == 的区别:

  • == 判断两个 是否相等,equals 判断两个对象是否等价
  • 对于基本数据类型,使用 == 判断两个变量的值是否相等,基本数据类型没用 equals 方法
  • 对于引用数据类型,使用 == 判断两个对象的引用地址是否相等,使用 equals 判断两个对象是否等价

所以 Object 源码中比较的是对象的引用地址,一般该方式不能满足我们的比较需求,需要重写该方法,重写 equals 方法时,我们一般也要重写 Object 的 hashCode 方法

演示

public class Cat {
    String name;
    
    public Cat() {}

    public Cat(String name) {
        this.name = name;
    }
    
    // 重写 equals,当Cat中 name相等,两个cat对象就是等价的
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cat cat = (Cat) o;
        return Objects.equals(name, cat.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

hashCode()

hashCode() 方法返回 哈希码值equals() 是用来判断两个对象是否等价。等价的两个对象的 哈希码值 一定相等,但是 哈希码值 相同的两个对象不一定等价。

所以我们重写 equals 方法时,总是一起重写 hashCode 方法,保证等价的两个对象的哈希码值也相等。

如果我们只重写了 equals 方法,没有重写 hashCode 方法,那么我们新建两个 Cat 对象,存储到 HashSet 集合中,我们希望 Cat 的 name 一致,只在集合中添加一个对象,因为没有实现 hashCode 方法,所以集合添加了两个对象

演示代码

public class Cat {
    String name;
    public Cat() {}
    public Cat(String name) {
        this.name = name;
    }
    // 重写 equals,当Cat中 name相等,两个cat对象就是等价的
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cat cat = (Cat) o;
        return Objects.equals(name, cat.name);
    }
}
public static void main(String[] args) {
    Cat c1 = new Cat("招财猫");
    Cat c2 = new Cat("招财猫");
    HashSet<Cat> list = new HashSet<>();
    list.add(c1);
    list.add(c2);
    System.out.println(list.size()); // 2 
}

clone()

1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

Cat c1 = new Cat("招财猫");
// c1 c2 = c1.clone(); // 'clone()' has protected access in 'java.lang.Object'
// 会报错提示 这个方法是被保护的

我们必须重写 clone() 方法:

@Override
protected Cat clone() throws CloneNotSupportedException {
    return (Cat)super.clone();
}
public static void main(String[] args) {
    Cat cat = new Cat();
    try {
        Cat c2 = cat.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    // java.lang.CloneNotSupportedException

}

但是调用 重写后的 clone 方法,会抛出 CloneNotSupportedException 异常,这是因为 Cat 类没有实现 Cloneable 接口。

clone() 并不是 Cloneable 接口定义的方法,Cloneable 只是规范,一个类重写了clone() 方法,没有实现 Cloneable 接口又调用了 clone(),就会抛出异常

public class Cat implements Cloneable {
    // ...
    @Override
    protected Cat clone() throws CloneNotSupportedException {
        return (Cat)super.clone();
    }
}

2. 浅拷贝

浅拷贝的概念以及特性:

  • 对于基本数据类型的成员变量,浅拷贝会直接该成员变量的 复制一份给新的对象,所以是两份新的不同数据,修改其中一个变量的值,并不会改变另一个变量的值
  • 对于引用数据类型的成员变量,浅拷贝会直接将该变量保存的 内存地址 复制一份给新的对象,比如类中有数组成员变量,那么就会将该变量存储的数组地址,给新的对象,所以两个对象的成员变量引用的都是同一个实例,修改其中一个变量的值,另一个变量的值也会跟着改变

浅拷贝实现

提示

Object中的 clone 方法是浅拷贝

3. 深拷贝

深拷贝的概念以及特性:

  • 对于基本数据类型的成员变量,和浅拷贝一样
  • 对于引用数据类型的成员变量,需要复制每一个 引用类型的成员变量所 引用的对象,就是递归拷贝,直到全部复制,总的来说就是,对象进行深拷贝要对整个对象图进行拷贝!

4. 重写 clone() 的规则

由于浅拷贝并不能保证clone出的对象和原对象完全独立,所以在很多时候会导致这样那样的问题,子类覆盖clone一般都是实现深拷贝

  • 首先调用父类super.clone方法(父类必须实现clone方法),这个方法最终会调用Object中的clone方法完成浅拷贝。
  • 对类中的引用类型进行单独拷贝。
  • 检查clone中是否有不完全拷贝,进行额外的复制。

5. clone() 的代替方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone()。

代替方案:

  • 拷贝构造函数
  • 拷贝工厂
  • 通过对象序列化实现深拷贝

关键字

this

提示

this 关键字表示 当前对象本身,可以调用对象的成员变量、成员方法,解决变量同名引起的二义性;可以在构造器中调用本类其他的构造器,但是必须是在第一句代码

内存图

mark
mark

super

  • super 关键字表示 当前父类,子类要访问父类成员时一定使用super,调用父类的成员变量、成员方法
  • super只是一个关键字,内部没有引用(地址)
  • super 调用构造方法必须写在子类构造方法的第一句
  • 如果子类构造方法没有显式调用父类构造方法时,那么jvm会默认调用父类的无参构造super()

static

1. 静态变量

  • 静态变量:又称为类变量,被 static 修饰的变量属于 类,类的所有实例对象都共享该变量,可以直接通过类名访问它;静态变量在内存中只存在一份
  • 实例变量:每创建一个实例化对象就会产生一个实例变量,实例对象销毁,变量也销毁。
public class StaticDemo {
    private int num; // 实例变量
    public static int age; // 静态变量
    // ...
}
public static void main(String[] args) {
    // Non-static field 'num' cannot be referenced from a static context
    // int num = StaticDemo.num;  
    StaticDemo staticDemo = new StaticDemo();
    int num = staticDemo.num;
    int age = staticDemo.age;
}

2. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract),并且只能访问所属类的静态字段静态方法,方法中不能有 thissuper 关键字。

public class StaticDemo {
    private int num; // 实例变量
    public static int age; // 静态变量
    // ...

    public static void run() {
        // Non-static field 'num' cannot be referenced from a static context
        System.out.println(num); 
        // 'StaticDemo.this' cannot be referenced from a static context
        System.out.println(this.num); 
        System.out.println(age);
    }
}

3. 静态语句块

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

static {
    System.out.println("我是静态语句块");
}
public static void main(String[] args) {
    StaticDemo s1 = new StaticDemo();
    StaticDemo s2 = new StaticDemo();
}
// 输出: 我是静态语句块

4. 静态内部类

非静态内部类依赖于外部类的实例,而静态内部类不需要。

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

public class StaticDemo {
    private int num; // 实例变量
    public static int age; // 静态变量
    // ...
    class InnerClass {
    }

    static class StaticInnerClass {
        void print() {
            // 不能访问非静态变量和方法
            // System.out.println(num);
            // run();
        }
    }
}
public static void main(String[] args) {
    StaticDemo staticDemo = new StaticDemo();
    // 依靠外部类实例化
    InnerClass innerClass = staticDemo.new InnerClass();
    // 不依靠外部类
    StaticInnerClass staticInnerClass = new StaticInnerClass();
}

5. 静态导包

import static com.xxx.ClassName.*;

6. 初始化顺序

提示

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

最后才是构造器初始化

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

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

final

提示

final 称之为最终的意思,可以修饰类、方法、局部变量、成员变量

  • final 修饰类不可以被继承
  • final 修饰的方法不能被子类重写
  • final 修饰的变量,只能初始化一次
  • final 修饰 基本数据类型,表示 值 不能被修改
  • final 修饰 引用数据类型,表示 引用的地址 不能改变
// final 修饰的类不可以被继承
public class FinalExample {

    // final修饰的变量只能初始化一次
    // final修饰的引用数据类型 不能改变引用地址
    final int num = 10;

    // final 修饰的方法也不能被重写
    public final void print() {
        System.out.println("我是 final print 方法");
    }

}

内部类

内部类概念

提示

什么是内部类,把一个类定义在另一个类内部,把里面的类称之为 内部类 ,把外面的类称为 外部类

public class Outer { // 外部类
    class Inner { // 内部类
        
    }
}

内部类可以看作和字段、方法一样是外部类的成员,成员可以用修饰符修饰:

  • 静态内部类:使用 static 修饰的内部类,访问内部类直接用外部类名访问
  • 实例内部类:没有 static 修饰的内部类,访问内部类需要实例化对象来访问
  • 方法内部类:定义在方法(局部)的类
  • 匿名内部类:特殊的局部内部类,适用于只用一次的类

对于每个内部了,Java编译器都会生产 class 文件:

  • 静态和实例内部类:外部类名$内部类名字
  • 局部内部类:外部类名$数字内部类名称
  • 匿名内部类:外部类名$数字

实例内部类

一般而言,成员内部类的访问修饰符是默认访问权限(包访问权限),开发时,可以根据需要添加具体
的访问权限

public class InnerExample {
    private String name;
    // *特性* 实例内部类:可以直接访问外部类的私有变量
    class Inner1{
        public void print() {
            System.out.println(name);
        }
    }
}

静态内部类

public class InnerExample {
    private static String staticName;
    // *特性* 静态内部类:可以直接访问类内部的静态私有成员
    static class Inner2{
        public void print() {
            System.out.println("staticName = " + staticName);
        }
    }
}

方法内部类

public class InnerExample {
    private static String staticName;
    public void print() {
        String str = "局部bl";
        // *特性* 局部内部类:可以直接访问方法内部的变量
        class Inner3 {
            public void print() {
                System.out.println(str);
            }
        }
    }
}

匿名内部类

提示

当一个类只使用一次,就可以声明成匿名内部类;匿名内部类必须有 实现 存在

// 父类
public abstract class Animal {
    abstract void eat();
}
// 接口
public interface IFly {
    void fly();
}
public class Test {
    public static void main(String[] args) {
        // 子类匿名内部类
        new Animal() {
            @Override
            void eat() {
                System.out.println("狗吃屎");
            }
        }.eat();

        // 实现类内部类(使用匿名内部类第二种方法)
        IFly iFly = new IFly() {
            @Override
            public void fly() {
                System.out.println("我会飞...");
            }
        };
        iFly.fly();
    }
}

参考资料