基础

4/8/2022 JavaSE

# 概述

关于 Java 相关概念介绍 可以参考:What is Java? (opens new window)

# Java语言重大事件

  1. 1995 正式发布

  2. 1996 正式发布了可以下载的 JDK 工具包 JDK 1.0

  3. 1999 发布第二代 Java 平台,简称 JDK1.2,细化三个不同的版本

    版本 英文名 简称
    标准本 Standard Edition J2SE
    企业版 Enterprise Edition J2EE
    微型版 Micro Edition J2ME
  4. 2004 JDK1.5 版本,添加了很多特性,比如 for-each 循环、泛型等,同时 JDK1.5 改名为 JavaSE5.0

  5. 2005 JDK6 版本

  6. 2009 Oracle 公司以 74亿$ 收购了 SUN 公司

# Java语言的特点

  1. 跨平台性

    所谓的跨平台,就是一套代码可在不同的操作系统上运行。

    现在思考一个问题:为什么它能够实现跨平台呢?

    答:因为有一个东西,叫做 JVM(Java Virtual Machine) Java 虚拟机,Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

    现在再来思考一个问题:以 .class 结尾的文件(字节码文件)可以直接在计算机上运行吗?

    答:不能。因为一个文件要想运行,它需要对应的运行环境,比如:.doc 文件需要 office 办公软件、.txt 文件需要记事本打开等。所以 .class 文件要想运行它也得要有一个对应的运行环境,这个运行环境就是 JRE(Java Runtime Enviroment)

    在 Java 中, JRE 包含了 JVM

    而作为开发人员,仅仅有 JRE 肯定是不行的,因为 JRE 只是一个运行环境,它并不附带能够开发 Java 源代码的能力,所以我们需要有一个东西(工具)能够帮助我们进行 Java 源代码的开发,这个工具就是 JDK(Java Development Kit)。而现实中我们只需要下载 JDK 就可以了,因为 JDK 中包含了 JRE,JRE 中又包含了 JVM

    关于 JDK 下载地址 ,请点击我 (opens new window)

  2. 面向对象

  3. 简单性

  4. 健壮性(鲁棒性)

    异常处理、垃圾回收等

  5. 大数据开发相关

# Java类的书写规范

// Demo.java
public class Demo {
  public static void main(String[] args) {
    System.out.println("hello");
  }
}

当你在类名前加了一个权限修饰符 public 时,类名要与文件名一致。如上,我写了一个 public 类 Demo,那么该文件名就必须是 Demo.java,否则 JVM 在编译时就会报错

一个 .java 文件中至多有一个公共类

# Java数据类型

基本数据类型(8个)

类型 占用字节数/byte 所属类型
byte 1 整型-字节型
short 2 整型-短整型
int 4 整型
long 8 整型-长整型
float 4 浮点型-单精度
double 8 浮点型-双精度
char 2 字符型
boolean 18\frac{1}{8} 布尔类型

引用数据类型

  1. 数组
    1. 抽象类
    2. 接口
  2. 注解,相当于 JavaScript 中的装饰器

包装类

# 整型

  1. 字节型(byte)
  2. 短整型(short)
  3. 整型(int)
  4. 长整型(long)

byte 类型为例,占 1个 字节(8bit),即 0 0000000,共有 256种 组合。其中第一位表示符号位,0 表示正数,1 表示负数,范围为 -128 ~ 127。其它整型类似

short 类型,占 2个 字节(16bit),即 0 000000000000000,共有 512种 组合。其中第一位表示符号位,0 表示正数,1 表示负数,范围为 -256 ~ 255

# 浮点型

  1. 单精度浮点型(float)

    书写:值后跟 f 或 F,如 float f = 12.3F; 因为小数在常量池中默认是以双精度(double)进行存储的,你若直接将一个双精度的值赋值给一个单精度的变量,这在编译时是会报错的。

    而当你在小数后跟一个 f 或 F,编译器会作进一步的处理:将双精度的值以单精度进行存储

  2. 双精度浮点型(double)

    书写:double d = 12.4;

# 字符型

  1. char

    书写:英文单引号,如 char c = '9';

    占 2个 字节,使用 Unicode 编码

# 布尔型

  1. boolean

    书写:boolean flag = true;

    占 1 bit

# 探索赋值语句的底层原理

本质是 Java 程序在内存中是如何分布的(需要深入理解 JVM 啦)

byte a = 33; 为例

上述赋值语句执行过程分析:

  1. 在存储区的常量池中存储 33 常量,并且该常量值是以 32bit 进行存储的
  2. 在栈内存中开辟一个 byte 类型的内存空间,大小为 8bit,空间的名称为 a
  3. 然后从常量池中取出 33 赋值给 a 变量,但是在赋值的过程中会发生这样一个情况
    1. 从常量池中取出的 33 是 32bit 的,而赋值的变量是 8bit 的,按理来说,如果将 32bit 的值赋给 8bit 的变量是不可以的,但是由于 java 编译器底层做了一些处理,使得其可以完成赋值而不报错

# 常量与变量

# 常量

表示在程序运行过程中不能改变的值

基本类型的值都可以认为是常量,如 4、3.4、'a'、true 等

常量存储在常量缓冲区(常量池)中,有且只有一份

常量池中的值默认空间大小有两种:32bit、64bit,32bit 用于存储 int 类型,64bit 用于存储 double 类型

# 变量

变量指的是 在程序运行过程中可以改变的

变量是一个内存空间(容器)

变量在创建的时候必须指定数据类型,以及变量空间的名字

变量空间里的内容是可以改变的

# 类型转换

任何一个数据最终都是以二进制的形式呈现给计算机进行 不同数据类型之间的转换本质是看要转换的俩方在计算机底层上的二进制位数以及精确程度 布尔类型很特殊,不能与其他基本数据类型发生转化

# 同种数据类型之间的转换

直接进行赋值操作即可

byte a = 99;
byte c = a;

int num1 = 88;
int num2 = num1;

float a = 12.8F;
float b = a;

# 不同数据类型之间的转换

  • 「整型与整型」 或 「浮点型与浮点型」

    比较的是内存空间大小

    • 小数据类型赋值给大数据类型,直接进行
    // 整型与整型
    byte a = 9;
    int b = a;
    
    // 浮点型与浮点型
    float f = 12.3F;
    double d = f;
    
    • 大数据类型赋值给小数据类型,需要进行指定,该过程也称为强制类型转换
    // 整型与整型
    int a = 9;
    byte b = (byte)a; // 强制类型转换
    
    // 浮点型与浮点型
    double d = 12.3;
    float f = (float)d; // 强制类型转换
    
  • 整型与浮点型

    比较的是精确程度

    它们俩之间的转换比较的是精确程度,浮点型的精确程度更高,所以由整型(低精确度)可直接转化成浮点型(高精确度),而由浮点型**(高精确度)转换为整型(低精确度)**需要进行强制类型转换

    // 浮点型转换为整型
    float f = 11.2F;
    long b = (long)f; // 强制类型转换
    
    // 整型转换为浮点型,直接转换
    long a = 99;
    float f = a;
    
  • 整型与字符型

    int a = 99;
    char b = (char) a;// 强制类型转换
    
    char a = '我';
    int b = a;
    

# 运算符号

# 算数运算符

  1. 加:+
  2. 减:-
  3. 乘:*
  4. 除:/
  5. 求余:%
  6. 自增:++
  7. 自减:--

# 关系运算符

  1. 大于:>
  2. 大于等于:>=
  3. 小于:<
  4. 小于等于:<=
  5. 等于:==
  6. 不等于:!=

# 赋值运算符

  1. =
  2. +=
  3. -=
  4. *=
  5. /=
  6. %=

# 逻辑运算符

  1. 逻辑与:&
  2. 逻辑或:|
  3. 逻辑非:!
  4. 短路与:&&
  5. 短路或:||

# 位运算符

  1. 按位与:&
  2. 按位或:|
  3. 按位取反:~
  4. 按位异或:^
  5. 左移:<<
  6. 右移:>>
  7. 无符号右移:>>>

# 条件运算符

也称为三目运算符

  1. ?:

# instanceof运算符

# 面试难点

# 自增与自减运算符

int a = 1;
a = a++;
System.out.println(a);

++ 自增运算符属于算数运算符,一般算数运算符的优先级大于赋值运算符优先级 以上述代码为例

  1. ++ 在后,计算机底层会先对 a 变量进行备份,然后将 a 变量的值加1(不是备份的变量哦),最后将备份变量的值符给 a 变量,然后备份空间被销毁
  2. ++ 在前,计算机底层会先对 a 变量进行加1,然后对 a 变量进行备份,最后将备份变量的值符给 a 变量,然后备份空间被销毁 所以无论 ++ 在前还是在后,最终赋值给 a 变量的都是备份中的值

了解上述 ++ 运算符原理后再来看一个面试题,最终输出多少?

int a = 1;
for (int i = 0; i < 100; i++) {
  a = a++;
}
System.out.println(a);// 1

# 原码、反码、补码

正数的原码、反码、补码相同

负数的反码相对于原码符号位不动,其余位取反,补码相对于反码加1

以十进制 6-6 为例,它们的原码、反码、补码如下

6 -6
原码 00000000 00000000 00000000 00000110 10000000 00000000 00000000 00000110
反码 00000000 00000000 00000000 00000110 11111111 11111111 11111111 11111001
补码 00000000 00000000 00000000 00000110 11111111 11111111 11111111 11111010

计算机中不管是正数还是负数,都是以补码的形式进行存储的

我们现在思考一个问题:为什么要用补码的方式表示数字,而不用原码(直接转换的二进制数字)表示呢?

答:比如做这种运算 -2 + 1 = ? 时,若直接将十进制数字转换成二进制数字进行运算就会发生错误,如果采用补码的方式进行运算就不会了。你可以自己先转换成二进制试一试,再转换成补码的形式试试就明白了。所以,计算机之所以用补码的形式表示数字,就是为了数字间计算方便

了解上述内容之后,再去学习 按位与(&)按位或(|)按位异或(^)左移(<<)右移(>>)、无符号右移(>>>) 等位运算就很容易了

# 0.1+0.2==0.3?

要想解决这个问题,你需要掌握如下知识:

  1. 如何将十进制数转换成二进制数?
  2. 如何用科学计数法表示二进制数?
  3. 计算机中是如何表示二进制数的?

# 如何将两个变量的值进行转换

int a = 1; int b = 2; 为例

# 方式一

分析:利用了一个额外的存储空间,效率不是很高 ❌

int a = 1;
int b = 2;
int c = a; // 在栈内存中开辟一个额外的内存空间,空间变量名为 c
a = b;
b = c;

# 方式二

分析:不需要额外的空间,效率相对于**「方式一」**要高点,但是可能会发生空间越界,原因是 a+b 的值可能超过 int 类型的值范围

int a = 1;
int b = 2;
a = a + b; 
b = a - b; // b = (a + b) - b = a
a = a - b; // (a + b) - a = b

# 方式三

分析:利用 异或(^) 运算符,效率最高。异或运算规则:相同数进行 异或 为 0,不同数进行 异或 为 1。所以,相同的数进行异或一定为0,然后再与 另外一个数 进行异或,其结果一定等于 另外一个数,也就是 a^b^b 一定等于 a

int a = 1;
int b = 2;
a = a^b;
b = a^b; // 等价于 b = (a^b)^b = a^b^b = a;
a = a^b; // 等价于 a = (a^b)^a = a^b^a = b;

# 语法结构

# 顺序结构

# 分支结构

# 单分支if

# 多分支switch

# 循环结构

# for循环

# while循环

# do-while循环

# 数组

# 数组的定义

  1. 形式:

    数据类型[] 数组名

    也可以是 「数据类型 []数组名」「数据类型 数组名[]」 的形式

    但是一般来说,我们采用正规的写法进行数组的定义,即 数据类型[] 数组名

int[] arr = {1, 2, 3}
int []arr = {1, 2, 3}
int arr[] = {1, 2, 3}
// 以上三种方式结果都是一样的,但一般我们都是采用的第一种方式进行数组的定义
  1. 举例:
int[] arr
char[] arr1
boolean[] arr2 
String[] arr3

# 数组的初始化

# 静态初始化

有长度,有元素

// 方式一
int[] arr = {1, 2,4, 9, 7};

// 方式二
// 当你通过 new 的方式创建一个对象时,在堆内存中会申请开辟了一个块新的空间
int[] arr = new int[]{3, 9, 4, 1, 7};

# 动态初始化

有长度,没有元素,但是会有默认值,不同的元素类型其默认值是不一样的

int[] arr = new int[5]; // 创建一个长度为5的整型数组,默认值为 0
float[] arr = new float[5]; // 默认值为 0.0
String[] arr = new String[5]; // 默认值为 null

# 类的加载顺序

  1. 先加载父类

    1. 父类产生自己的静态空间,里面先加载静态属性、再加载静态方法、最后加载静态代码块
    2. 然后执行静态代码块中的代码
  2. 再加载子类

    1. 子类产生自己的静态空间,里面先加载静态属性、再加载静态方法、最后加载静态代码块
    2. 然后执行静态代码块中的代码
  3. 在堆中开辟的空间

  4. 然后加载父类的非静态属性非静态方法非静态代码块构造方法

    1. 先执行非静态代码块中的代码
    2. 再执行构造方法
  5. 接着加载子类的非静态属性非静态方法非静态代码块构造方法

    1. 先执行非静态代码块中的代码
    2. 再执行构造方法
  6. 对象创建成功后,将对象的地址(引用)赋给变量

public class Animal {
    private static String name = "Animal"; // 静态属性
    private int age = 12; // 非静态属性

    Animal(){
        System.out.println("我是 Animal 构造方法");
    }

    // 非静态代码块
    {
        this.animalNormalTest();
        System.out.println("我是 Animal 非静态代码块");
    }

    // 静态代码块
    static {
        Animal.animalTest();
        System.out.println("我是 Animal 静态代码块");
    }

    // 静态方法
    public static void animalTest(){
        System.out.println("我是 " + Animal.name  + " 类的静态方法 animalTest");
    }

    // 非静态方法
    public void animalNormalTest(){
        System.out.println("我今年 " + this.age + " 岁");
        System.out.println("我是 Animal 类的非静态方法 animalNormalTest");
    }

}

public class Cat extends Animal {
    private static String name = "Cat";// 静态属性
    private int age = 99;// 非静态属性

    Cat() {
        System.out.println("我是 Cat 构造方法");
    }

    // 非静态代码块
    {
        this.catNormalTest();
        System.out.println("我是 Cat 非静态代码块");
    }

    // 静态代码块
    static {
        Cat.catTest();
        System.out.println("我是 Cat 静态代码块");
    }

    // 静态方法
    public static void catTest() {
        System.out.println("我是 " + Cat.name + " 类的静态方法 catTest");
    }
    // 非静态方法
    public void catNormalTest() {
        System.out.println("我今年 " + this.age + " 岁");
        System.out.println("我是 Cat 类的非静态方法 catNormalTest");
    }
}
// 在主程序中执行 `Cat cat = new Cat()` 代码输出如下结果:
// 我是 Animal 类的静态方法 animalTest
// 我是 Animal 静态代码块
// 我是 Cat 类的静态方法 catTest
// 我是 Cat 静态代码块
// 我今年 12 岁
// 我是 Animal 类的非静态方法 animalNormalTest
// 我是 Animal 非静态代码块
// 我是 Animal 构造方法
// 我今年 99 岁
// 我是 Cat 类的非静态方法 catNormalTest
// 我是 Cat 非静态代码块
// 我是 Cat 构造方法
Last Updated: 4/10/2022, 10:38:38 PM