【Java】Java SE 笔记(未完待续)

一、Java 初步

1.1 Java 介绍

Java 的三大版本

  • Java SE(Java Platform,Standard Edition):Java 标准版。
  • Java EE(Java Platform,Enterprise Edition):Java 企业版。
  • JavaME(Java Platform,Micro Edition):Java 微型版。

JDK、JRE、JVM 的关系

  • JDK(Java Development Kit):Java 开发工具箱,包含 JRE、JVM。
  • JRE(Java Runtime Environment):Java 运行环境,包含 JVM。
  • JVM(Java Virtual Machine):Java 虚拟机,不可独立安装。

如果只部署运行 Java 程序,只安装一个 JRE 即可。

Java 语言特性

Java 语言是一种面向对象语言,具有简单性、面向对象、健壮性、多线程、可移植性、编译和解释性、安全性、高性能、分布性等特性。

  1. 简单性

    Java 底层采用 C++ 语言实现,丢弃了 C++ 中的一些难理解的特性。

  2. 健壮性

    Java 引入了自动垃圾回收机制(GC 机制),不易导致内存泄漏。

  3. 多线程

    Java 支持多个线程同时并发执行,同时也提供了多线程环境下的安全机制。

  4. 可移植性/跨平台

    Java 可以一次编写到处运行。Java 程序在 JVM 中运行,JVM 可以实现跨平台。

1.2 Java 快速入门

卸载 JDK 步骤

  1. 删除 JDK 所在目录。
  2. 删除 Java 的环境变量。

安装 JDK 步骤

  1. 下载安装 JDK。
  2. 配置环境变量 PATH(对应 Java/bin 路径)。
  3. cmd 中 java -version 验证是否成功。

注意:

  1. JAVA_HOME 环境变量:对应 Java 路径。后期用到 Tomcat 就必须配置了,目前阶段可配可不配。
  2. CLASSPATH 环境变量:类加载器默认从当前路径寻找加载字节码文件,若配置 CLASSPATH 环境变量,则从环境变量路径寻找。
  3. JDK8 及之前的版本安装时,不仅会内置一个 JRE,也会在目录外独立生成一个 JRE,JDK8 之后的版本仅有内置 JRE,若希望生成外部 JRE 需要特殊的命令。

Java 代码示例

public class Hello{//类体:所有有效代码必须在类体内
    //方法体中代码遵循自上而下的顺序依次执行
    public static void main(String[] args){//主方法入口:每类最多一个,公开类必须有主方法
    //主方法声明中唯一可以修改的是“args”字符数组变量名
        System.out.println("Hello World!");//语句最后必须以英文分号结尾
    }
}

编译运行 Java 代码步骤

  1. cmd 中 javac 文件名.java 编译生成 .class 文件(文件名和类名必须保持一致)。
  2. java 类名 运行代码。

注意:

  1. javac 后跟的是源文件路径(包括相对和绝对路径)。java 后跟的是类名(非路径),执行前需提前切到字节码文件所在目录。
  2. cmd 快速获取文件路径:将 Java 源文件直接拖到 cmd,cmd 就会获得该文件的路径。
  3. cmd 快速定位路径:直接在文件夹路径输入 cmd。
  4. 高版本 JDK 可以快速运行.java文件:输入 java 命令,再直接将源文件拖入 cmd 窗口中回车运行。这种方式不会产生.class文件,但是底层原理和正常是一样的。

Java 程序加载与执行的过程

Java 程序从开发到运行包括编译和运行两个阶段,这两个阶段可以在不同的操作系统中完成。

  1. 编译阶段需要使用 javac.exe(安装 JDK 即存在)命令。直接编写的源代码(普通文本)是无法被 JVM 识别的,必须经过编译,变成字节码文件(非二进制),JVM 才能识别。普通文本变成字节码的过程叫做编译。Java 源文件中的源代码如果不符合 Java 的语法机制,编译时编译器会提示错误信息,并且无法生成 .class 文件。反之则生成 .class 文件。.class 文件是最终要执行的程序,此时将 Java 源文件删除是不会影响 Java 程序运行的。一个源文件可能生成多个字节码文件。
  2. 运行阶段需要使用 java.exe(安装 JRE 即存在)命令。当编译阶段完成之后,使用 JRE 中的 java.exe 命令运行程序,例如:执行命令 java A,该命令执行后会启动类加载器,类加载器(类加载器默认从当前路径下寻找加载字节码文件)去硬盘上搜索 A.class 文件,找到该字节码文件之后,将其加载到 JVM 当中,JVM 中的解释器会将 A.class 字节码文件解释为操作系统可以执行的二进制码,然后操作系统通过执行二进制码和硬件平台交互。

运行 Java 程序的前提是当前操作系统上已经安装了对应版本的 JVM(JVM 不是单独安装的,安装 JRE 即可运行,不同的操作系统需要安装不同版本的 JRE,不同版本的 JRE 对应不同版本的 JVM)。

image.png

public class 和 class 的区别

  • 同一个 Java 源文件中最多只能存在一个 public class 且必须与文件名一致,但是可以存在多个 class。

  • public class 类中必须有主方法。

  • 在同一个 Java 文件中类名不能重名。

  • 在实际的开发中,比较规范的写法是一个 Java 源文件中只定义一个 class。

  • 只要有一个 class 的定义,在编译时就会生成一个 .class 文件。

  • 每个类中都可以编写主方法入口(每类最多一个),但实际开发中不需要多个入口。

    想让 A 类中的主方法执行,则执行:java A

注释

注释是对代码的解释和说明,注释内容将被 Java 编译器忽略,不会生成到 .class 文件中,只在 Java 源文件中保留。

注释的三种使用方法

  1. 单行注释 快捷键 ctrl+/
//这是单行注释
  1. 多行注释 快捷键 ctrl+shift+/
/*
这是第一行多行注释
这是第二行多行注释
*/
  1. javadoc 注释
/**
*这里的信息是javadoc注释
*@author 作者名字
*@version 版本号
*@since 从哪个版本开始存在
*javadoc的注释会被JDK下的bin目录的javadoc.exe解析并生成帮助文档
*使用IDEA也可以生成javadoc文档:一百度就有,命令行也可以生成
*/

注释应该怎么写

  • 通常要在类和接口上使用 javadoc 注释,标明:创建者,创建时间,版本,以及该类的作用。
  • 在方法中,对入参,出参,以及返回值,均要标明。
  • 对常量,使用多行注释,进行标明该常量的用途。
  • 在关键算法上,添加注释并且按照顺序依次标明,写明白该方法为什么这么做。
/*
 * HelloWorld Java 程序
 * @author Spring
 * @version 1.0
 * @since 1.0
 */
public class HelloWorld {
    /**
     * 这是程序的入口
     * @param args 参数
     */
    public static void main(String[] args) {
        // 向控制台输出一段话
        System.out.println("你好世界!");
    }
}

二、标识符和关键字

2.1 标识符

标识符是指程序员自己规定的代表一定含义的单词。例如:类名、接口名、变量名、方法名、常量名等。

标识符命名规则(必须遵守)

  • 标识符只能由数字、字母(包括中文)、下划线 _、美元符号 $ 组成,不能有空格。
  • 标识符不能以数字开始。
  • 标识符严格区分大小写。
  • 关键字和保留字不能作为标识符。
  • 标识符理论上没有长度限制。

注意:

  1. 保留字是为 Java 预留的关键字,在 Java 现有版本中没有特殊含义,以后版本可能会作为有特殊含义的词,或者该词虽然在 Java 中没有特殊含义,以后版本也不打算使用,但在其它语言中有特殊含义,不宜在 Java 中定义为变量名称等,因为容易混淆。
  2. 尽管标识符严格区分大小写,但对于类名,若同一个 Java 源文件同时出现 hello 类和Hello 类,那么谁在前就生成谁的 .class 文件。因此最好不要让类名“相同”。

标识符命名规范(通用习惯)

  • 见名知意:增强可读性。例如:Student 表示学生类型。
  • 驼峰命名:利用首字母大小写分隔单词,每个单词之间会划清界限。
  • 类名、接口名:首字母大写,后面每个单词首字母大写,例如:HelloWorld。
  • 变量名、方法名:首字母小写,后面每个单词首字母大写,例如:getName。
  • 常量名:全部大写,单词和单词之间使用下划线 _ 连接,例如:MAX_NUMBER。
  • 包名:字母全部小写,使用 . 连接。例如:com.spring1227.hymall。

2.2 关键字

关键字是编译语言事先定义的,有特殊意义的固定单词,不能在程序中做其他目的使用。

  • Java 中所有关键字都是小写的。

常见关键字

byte short int long float
double boolean char if for
else while do continue break
public default protected private throw
throws try catch final finally
case switch transient package new
static this abstract strictfp native
goto super extends implements import
instanceof return synchronized void const
class enum assert interface volatile

三、变量

变量是内存中存储数据的最基本的单元。

  • 变量的三要素是数据类型、变量名、字面量。
  • 变量存储的值是可以被改变的。

字面量就是数据、数值。在编程语言中数据被分为:整数型、浮点型、布尔型、字符型、字符串型。

变量的使用

  • 使用变量前必须先声明。
  • 局部变量使用前必须先初始化。
  • 同一个域中,变量不能重名。

声明变量的语法格式:

数据类型 变量名;
变量名 = 值;

int i;
i = 1;

int count = 100;//声明的同时赋值

int a = 200,b = 300,c = 400;//Java允许同时命名赋值三个变量

int a,b,c = 20;//在Java中这种声明赋值方式是错的,但是在C++中是对的

变量的分类

变量根据声明位置分为两大类:

  • 局部变量:在方法体中声明的变量以及方法中每一个参数都是局部变量。局部变量只在方法体中有效。

  • 成员变量:在方法体外,类体内声明的变量是成员变量。

    • 静态(成员)变量:使用 static 关键词修饰的称为静态成员变量。
    • 实例(成员)变量:没有 static 关键词修饰的称为实例成员变量。
public class Test{
	int x = 20;//实例变量x
 	static int y = 200;//静态变量y
 
 	public static void  sum(int a,int b){//局部变量a和b
	    int i = 100;//局部变量i
	}
}

变量的作用域

变量的作用域指变量的使用范围。

  • 不同类型的变量存储在 Java 虚拟机上的不同内存区域,所以变量是有作用域的。先简单理解为:出大括号就不认识。
  • Java 遵循就近原则。
{
    int i = 100;
    {
	这里可以访问i
    }
}
{
    这里无法访问i。
}
public class Test {
    static int k = 100;
    public static void main(String[] args){
        int k = 300;
        System.out.println("main k = " + k);//Java程序遵守就近原则
        m();
    }
    public static void m(){
        System.out.println("m() k = " + k);
    }
}
/*
main k = 300
m() k = 100
*/

变量的存储区域

image.png

三、数据类型

3.1 数据类型介绍

根据数据类型决定在程序运行阶段给该变量分配多大的内存空间。

数据类型的分类

数据类型主要分为两大类:

  • 基本数据类型:

    • 整数型:byte、short、int、long。
    • 浮点型:float、double。
    • 字符型:char。
    • 布尔型:boolean。
  • 引用数据类型:除基本数据类型其他均为引用数据类型。

基础数据类型的详细信息

数据类型 占字节数(byte) 取值范围 缺省默认值
byte 1 [ -27~ 27-1]、[ -128 ~ 127] 0
short 2 [ -215 ~ 215-1]、[ -32768 ~ 32767] 0
int 4 [ -231 ~ 231-1]、[ -2147483648 ~ 2147483647] 0
long 8 [ -263 ~ 263-1] 0L
float 4 [ -231 ~ 231-1] 0.0f
double 8 [ -263 ~ 263-1] 0.0
boolean 1 true、false false
char 2 [ 0 ~ 216-1]、[ 0 ~ 65535] '\u0000'

关于计算机存储单位:

1bit就是一个 1 或 0
1字节 = 8bit
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB

byte b = 2; 在计算机中表示为:00000010
short s = 2; 在计算机中表示为:00000000 00000010
int i = 2; 在计算机中表示为:00000000 00000000 00000000 00000010

字符型数据类型

  • char 占用 2 个字节。
  • char 类型的字面量用单引号 ' 括起来,只能存储一个字符。
  • char 类型采用 unicode 编码方式。
  • Java 语言中的 char 类型变量可以容纳一个汉字。一个汉字刚好占用 2 个字节。
public class Test {
    public static void main(String[] args) {
        char c1 = '中';
        System.out.println(c1);//中
        char c2 = 'a';
        System.out.println(c2);//a
        char c3 = '0';
        System.out.println(c3);//0
//        char c4 = "a"; String类型不能赋值给char类型
//        System.out.println(c4);
//        char c5 = 'ab'; char类型只能容纳一个字符
//        System.out.println(c5);
//        char c6 = 'a; 未结束的字符文字
//        System.out.println(c6);
    }
}
public class Test {
    public static void main(String[] args) {
        char c1 = 97;
        System.out.println(c1);//a
        char c2 = 'a' + 1;//acsll码97+1=98,98代表b
        System.out.println(c2);//b
        char c3 = '0' + '1';//ascll码48+49=97,97代表a
        System.out.println(c3);//a
    }
}

ACSLL码中 数字 0 对应ACSLL码 48,字母 a 对应ACSLL码 97,字母 A 对应ACSLL码 65。

转义字符

  • 转义字符是一种特殊的字符常量。
  • 转义字符以反斜线 \ 开头,后跟一个或几个字符。

常用转义字符:

符号 字符含义 符号 字符含义
\n 换行 \t 制表符
\r 回车 \" 双引号
\f 换页符 \' 单引号
\b 退格 \|反斜杠
\0 空字符 \ddd 八进制字符
\s 空格 \uxxx 16进制Unicode字符
public class Test {
    public static void main(String[] args) {
        char c1 = 't';
        System.out.println(c1);
        char c2 = '\t';// \t是一个字符,两个字符合在一起代表一个字符,非字符串,表示“制表符tab”
        System.out.println(c2);
        System.out.println("abc\tdef");
        System.out.print("123\n456");//print()表示仅打印不换行
        System.out.println("\\");//输出一个"\"
        char c3 ='\u4e2d';//unicode编码表示"中"
        System.out.println(c3);
    }
}
/*
t

abc	def
123
456
中
*/

整数型数据类型

整数型包括:byte、short、int、long。

  • 在任何情况下,整数型的字面量默认被当作 int 类型处理。
  • 一般定义整数型直接用 int 即可,不必过于纠结。
  • 如果希望整数型被当作 long 类型处理,需要在字面量后加“L / l”。
  • 整数型支持四种表达方式:十进制、八进制、十六进制、二进制。
public class Test {
    public static void main(String[] args){
        System.out.println("十进制:" + 10);//十进制:10
        System.out.println("八进制:" + 010);//八进制:8
        System.out.println("十六进制:" + 0x10);//十六进制:16
        System.out.println("二进制:" + 0b10);//二进制:2
    }
}

浮点型数据类型

浮点型包括:float,double。

  • 在任何情况下,浮点型的字面量默认被当作 double 类型处理。
  • 如果希望浮点型被当作 float 类型处理,需要在字面量后加“F / f”。
  • 浮点型数据在计算机中存储的都是近似值,因为无限小数无法存真正存储。
  • 在银行方面 double 精度也是远远不够的,一般使用 java.math.BigDecimal,属于引用数据类型。
  • long 类型 8 个字节,float 4个字节,但是 float 容量更大。

布尔型数据类型

布尔型包括:boolean。

  • boolean 只有两个值:true 和 false。
  • 布尔类型一般是充当条件的。
  • 布尔类型不能和其它类型转换。

字符串型数据类型

  • String 类型的字面量用双引号 " 括起来。
  • String 类型是引用数据类型,非基本数据类型。

3.2 数据类型转换

在 Java 中小容量可以直接赋值给大容量,这个过程称为自动类型转换。

  • 容量从小到大的排序为:byte < short(char) < int < long < float < double。
  • 其中 short 和 char 都占用 2 个字节,但是 char 可以表示更大的正整数。因为 char 表示的是文字,没有正负之分。
  • 基本数据类型中除了布尔类型以外,都可以互相转换。
public class Test {
    public static void main(String[] args){
        long c = 10;//小转大,发生了自动类型转换
	long d = 10L;//不存在类型转换
	long e = 2147483648;//会报错,因为2147483648超出了int值范围,需要改为2147483648L
        System.out.println(c);//10
    }
}

在 Java 中大容量转换成小容量,必须手动添加强制类型转换符 (数据类型),这个过程称为强制类型转换。

  • 强制类型转换可能会导致精度损失。
public class Test {
    public static void main(String[] args){
        long c = 10;
        int d = (int)c;//强制转换为int类型
        System.out.println(d);//10

        int a = 300;
        byte b = (byte)a;//300超过了byte的范围,造成了精度损失
        System.out.println(b);//44

	int e = 150;
        byte f = (byte)e;
        System.out.println(f);//-106 
    }
}
/*
int 类型的 300 对应的二进制码是:00000000 00000000 00000001 00101100,
    强制类型转换的时候变成 1 个字节,底层是前 3 个字节砍掉了,
    也就是最后的二进制码是:00101100,这个二进制码对应的是 44。

int 类型的 150 对应的二进制是:00000000 00000000 00000000 10010110,
    强转时前 3 个字节砍掉,最终计算机存储的二进制为:10010110,计算机中最终存储的的是二进制补码形式,
    也就是说 10010110 现在是二进制补码形式,得出原码:11101010,而这个值就是 -106。
*/

注意:

关于计算机中原码反码补码:

计算机存储数据是以二进制补码形式存储的。在计算机中,一个二进制最左边是符号位,0 表示整数,1 表示负数。

正数的原码反码补码是一样的。
负数的反码是在其原码的基础上,符号位不变,其余各位取反。
负数的补码是在其原码的基础上,符号位不变,其余各位取反后加1(即在反码的基础上加1)。补码再补一次即原码
【+1】= 原: [0000 0001] = 反:[0000 0001] = 补:[0000 0001]
【-1】 = 原:[1000 0001] = 反:[1111 1110] = 补:[1111 1111] = 再补:[1000 0001]

当一个整数型的字面量没有超出 byte、short、char 的取值范围,可以将该字面量直接赋值给 byte、short、char 类型的变量,如果超出范围则需要添加强制类型转换符。

public class Test {
    public static void main(String[] args){
        byte a = 1;
        System.out.println(a);//1
        byte b = 128;//报错,需要强转
        System.out.println(b);

        short c = 32767;
        System.out.println(c);//32767
        short d = 32768;//报错,需要强转
        System.out.println(d);

        char e = 97;//ascll码转为字符a
        System.out.println(e);//a
        char f = 65536;//报错,需要强转
        System.out.println(f);
	char g = 65535;
        System.out.println(g);//? 答案是一个乱码“?”,是一个字符,由于字符集不同导致乱码
    }
}

byte、short、char 任两个或者三个做混合运算时,先各自转换为 int 再做运算。

char c = 'a';//acsll码对应97
byte b = 1;
short s = c + b;//会报错,int转short需要强转。当右侧是变量时,需要加强转才行:(c + b)为int类型

short k = 98;//当右侧是具体数值时,可以自动转

多类型混合运算,各自转为容量最大的再运算(byte、short、char 除外,会各自转为 int 再运算)。

public class Test {
    public static void main(String[] args) {
      long a = 10L;
      char c = 'a';
      short s = 100;
      int i = 30;
      System.out.println(a + c + s + i);//237 多种类型做混合运算时,各自先转换成容量最大的再运算,此处转为long

      //int x = a + c + s + i;
      //System.out.println(x);
      //报错!java: 不兼容的类型: 从long转换到int可能会有损失
      //说明在运算时(a + c + s + i)自动转换为long类型了
   
       int x = c + s + i;
       System.out.println(x);
       //结果为:227未报错,因为char、short、byte运算时自动转为int
    }
}

Java中返回变量类型的方法:

public class Test {
    public static void main(String[] args) {
        long a = 10L;
        char c = 'a';
        short s = 100;
        int i = 30;
        System.out.println(getType(a+c+s+i));
	//class java.lang.Long
    }
    public static String getType(Object o){ //获取变量类型方法
        return o.getClass().toString(); //使用getClass()方法
    }
}

类型转换小案例:

int a = 100;//没有类型转换
long b = 100;//自动类型转换:int转为long
long c = 100L;//没有类型转换
long d = 2147483648;//会报错,int值超范围,此时必须加“L”变为long类型
long e = 2147483648L;
int f = (int)e;//强制类型转换,超int范围会损失精度
byte g = 127;//自动类型转换
short h = 2;//自动类型转换
int m = 10/3;//可以 10/3为3
short n = (byte)e/3//报错,结果为int不能转为short。e转为byte进行/3运算又转为int
char cc = 'a';
System.out.println((byte)cc);//97
int o = cc + 100;//cc先自动转为int再运算
System.out.println(o);//197

四、运算符

运算符是对操作数的运算方式。

运算符的分类

按照操作数分类有单目运算符、双目运算符、三目运算符。

按照功能来分:

  • 算术运算符:+、-、*、/、%、++、 --。
  • 关系运算符:>、>=、<、<=、==、!=。
  • 逻辑运算符:&、|、!、&&、||。
  • 赋值运算符:=、+=、-=、*=、/=、%=、^=、&=、|=、<<=、>>=。
  • 位运算符:&、|、^、~、<<、>>、>>>。
  • 条件运算符:布尔表达式 ? 表达式 1 : 表达式 2。
  • 字符连接运算符:+。
  • 其他运算符:instance、new。

算术运算符

操作符 描述 例子(A=10,B=20)
+ 加法:相加运算符两侧的值 A + B 等于 30
- 减法:左操作数减去右操作数 A – B 等于 -10
* 乘法:相乘操作符两侧的值 A * B 等于 200
/ 除法:左操作数除以右操作数 B / A 等于 2
% 取余:左操作数除以右操作数的余数 B % A等于 0
++ 自增:操作数的值增加1 B++ 或 ++B 等于 21
-- 自减:操作数的值减少1 B-- 或 --B 等于 19
//前缀表达式:先赋值,再自增减。
int a = 1;
int b = a++;//先做b = a,再做a = a + 1
//b = 1,a = 2

//后缀表达式:先自增减,再赋值。
int x = 1;
int y = ++x;//先做x = x + 1,再做y = x
//y = 2,x = 2
public class Test {
    public static void main(String[] args) {
        int a = 7;
        int b = 3;
        System.out.println(a + b);//10
        System.out.println(a - b);//4
        System.out.println(a * b);//21
        System.out.println(a / b);//2
        System.out.println(a % b);//1
        int x = 10;
        System.out.println(x++);//10 先赋值,再自加
        System.out.println(x);//11
        int y = 5;
        System.out.println(++y);//6 先自加,再赋值
        System.out.println(y);//6
    }
}

关系运算符

运算符 描述 例子(A=10,B=20)
== 两个操作数的值相等,则为真 (A == B)为假
!= 两个操作数的值不等,则为真 (A != B) 为真
> 左操作数的值大于右操作数的值,则为真 (A > B)为假
< 左操作数的值小于右操作数的值,则为真 (A < B)为真
>= 左操作数的值大于等于右操作数的值,则为真 (A >= B)为假
<= 左操作数的值小于等于右操作数的值,则为真 (A <= B)为真
  • 关系运算符的运算结果都是布尔类型。
  • 关系运算符中两个符号的中间不能有空格。
public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 5;
        System.out.println(a == b);//false
        System.out.println(a != b);//true
        System.out.println(a > b);//true
        System.out.println(a >= b);//true
        System.out.println(a < b );//false
        System.out.println(a <= b);//false
  
    }
}

逻辑运算符

操作符 描述 例子
& 逻辑与运算符:当且仅当两个操作数都为真,则为真 (A & B)为假
| 逻辑或操作符:任何两个操作数任何一个为真,则为真 (A|B)为真
! 逻辑非运算符:用来反转操作数的逻辑状态 !(A&&B)为真
&& 短路与运算符:前操作数为false时,结果必false,就不再判断后操作数 (A & B)为假
|| 短路或运算符:前操作数为true时,结果必true,就不再判断后操作数 (A||B)为真
  • 逻辑运算符两边必须为布尔类型,并且结果也是布尔类型。
  • 如果把两边操作数改为int类型,那么 &、| 会被认为是位运算符。
public class Test {
    public static void main(String[] args) {
        boolean a = true;
        boolean b = false;
        System.out.println(a&b);//false
        System.out.println(a|b);//true
        System.out.println(!a);//false
        System.out.println(a&&b);//false
        System.out.println(a||b);//true
    }
}

赋值运算符

操作符 描述 例子 操作符 描述 例子
= 赋值运算符 C = A + B <<= 左移位赋值运算符 C << = 2
+= 加和赋值操作符 C + = A >>= 右移位赋值运算符 C >> = 2
-= 减和赋值操作符 C - = A &= 按位与赋值运算符 C&= 2
*= 乘和赋值操作符 C * = A ^= 按位异或赋值操作符 C ^ = 2
/= 除和赋值操作符 C / = A |= 按位或赋值操作符 C |= 2
%= 取模和赋值操作符 C%= A
  • 赋值运算符“=”:将右操作数的值赋给左侧操作数。
  • 扩展赋值运算符书写时两个符号间不能有空格。
  • 使用扩展赋值运算符时,不会改变运算结果数据类型。

:x = x +1 和 x += 1 相似,但是本质不同,不能等价。

public class Test {
    public static void main(String[] args) {
        byte x = 100;
        x = x + 1;//报错!x + 1默认为int类型,转换为byte会有损失
        System.out.println(x);
        x += 1;//等价于x = (byte)(x+1)
        System.out.println(x);//101
    }
}
//上文的x += 1中的x生来就是byte类型。

位运算符

位运算符用来对二进制位进行操作。

位运算符:

操作符 描述 例子(A=0011 1100,B=0000 1101)
按位与运算符:如果相对应位都是1,则结果为1,否则为0 (A&B),得到12,即0000 1100
| 按位或运算符:如果相对应位都是 0,则结果为 0,否则为 1 (A | B)得到61,即0011 1101
^ 按位异或运算符:如果相对应位值相同,则结果为0,否则为1 (A ^ B)得到49,即 0011 0001
按位取反运算符:反转操作数的每一位 (〜A)得到-61,即1100 0011
<< 按位左移运算符:左操作数按位左移右操作数指定的位数 A << 2得到240,即 1111 0000
>> 按位右移运算符:左操作数按位右移右操作数指定的位数 A >> 2得到15即1111
>>> 按位右移补零操作符:左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充 A>>>2得到15即0000 1111
public class Test {
    public static void main(String[] args) {
        int a = 60;//60 = 0011 1100
        int b = 13;//13 = 0000 1101
        int c = 0;
        c = a & b;//12 = 0000 1100 
        System.out.println("a & b = " + c );//a & b = 12
  
        c = a | b;//61 = 0011 1101 
        System.out.println("a | b = " + c );//a | b = 61
  
        c = a ^ b;//49 = 0011 0001 
        System.out.println("a ^ b = " + c );//a ^ b = 49
  
        c = ~a;//-61 = 1100 0011 
        System.out.println("~a = " + c );//~a = -61
  
        c = a << 2;//240 = 1111 0000 
        System.out.println("a << 2 = " + c );//a << 2 = 240
  
        c = a >> 2;//15 = 1111 
        System.out.println("a >> 2  = " + c );//a >> 2  = 15
  
        c = a >>> 2;//15 = 0000 1111 
        System.out.println("a >>> 2 = " + c );//a >>> 2 = 15
    }
}

条件运算符

条件运算符:布尔表达式 ? 表达式1 : 表达式2

  • 如果为true,执行表达式1,否则执行表达式2。
  • 条件运算符不是语句。
sex ? '男' : '女';//是错误的,不是Java语句
char c = sex ? '男' : '女';//这样是对的,前面的类型要对应,但是实际开发不这么写
System.out.println(sex?'男':'女');//这样也是对的,在这里的括号里什么数据类型都能放
public class Test {
    public static void main(String[] args) {
        boolean sex = true;
        System.out.println(sex?'男':'女');//男
    }
}

字符串连接运算符

字符串连接运算符:+。

  • 当+两边都是数字类型,做加法运算。
  • 当+两边有一边为字符型,做拼接操作。
  • 遵循自左向右的顺序执行。
public class Test {
    public static void main(String[] args) {
        System.out.println(100+200+"200");//300200
        System.out.println(300+200);//500
        System.out.println(300+(100+"200"));//300100200
        System.out.println(100+"+"+200+"="+100+200);//100+200=100200 遵循自左向右的顺序,除非有小括号
        System.out.println(100+"+"+200+"="+(100+200));//100+200=300
    }
}

接受键盘输入

public class Test {
    public static void main(String[] args) {
        //创建一个键盘扫描器
        java.util.Scanner s = new java.util.Scanner(System.in);
        //从键盘接收一个int类型的数据
        int a = s.nextInt();
        System.out.println(a);
        //从键盘接收一个字符串类型的数据
        String b = s.next();
        System.out.println(b);
    }
}
/*
10
10
你好
你好
*/

以导包形式接收键盘数据

import java.util.Scanner;
public class Test {
    public static void main(String[] args) {
        //创建键盘扫描器对象
        Scanner s = new Scanner(System.in);
        //从键盘接收一个int类型的数据
        int a = s.nextInt();
        System.out.println(a);
    }
}
/*
1
1
*/

控制语句

控制语句是用来对程序流程的选择、循环、转向和返回等进行控制的。

  • 所有控制语句都是可以嵌套使用的。

Java中共有四类8种控制语句:

  • 选择语句:if、switch。
  • 循环语句:for、while、do ... while。
  • 转向语句:break、continue。
  • 返回语句:return。

选择语句

选择语句又称分支语句,它用来对给定的条件进行判断,从而决定执行两个或多个分支中的哪一支。

if语句

  • if语句在任何情况下都只有一个分支进行。
  • 当分支中只有一条 java 语句话,大括号可以省略不写(不建议)。

if语句的语法格式:

//第一种
if(布尔表达式){
	java语句;
}

//第二种
if(布尔表达式){
	java语句;
} else {
	java语句;
}

//第三种
if(布尔表达式){
	java语句;
}else if(布尔表达式){
	java语句;
} else {
	java语句;
}

//第四种
if(布尔表达式){
	java语句;
}else if(布尔表达式){
	java语句;
} else if(布尔表达式){
	java语句;
}
public class Test {
    public static void main(String[] args) {
        boolean a = true;
        String b = "";
        if(a){
            b = "男";
        } else {
            b = "女";
        }
        System.out.println(b);//男
    }
}

switch语句

  • switch()中支持int、String类型,由于byte、short、char会自动转int,因此也是可以的。
  • switch语句中case可以进行合并。
  • switch语句中case后要求是常量。

switch的语法格式为:

switch(变量){
	case 字面量:
		java语句;
		break;//非必须
	case 字面量:
		java语句;
		break;//非必须
	default://非必须
		java语句;
}
import java.util.Scanner;
public class Test {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.print("请输入[1-7]的整数数字:");
        int dayOfWeek = scan.nextInt();
        switch(dayOfWeek){
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
                System.out.println("今天是工作日哦!");
                break;
            case 6:
                case 7:
                System.out.println("今天是休息日哦!");
                break;
            default :
                System.out.println("对不起,您的输入有误");
        }
    }
}

/*
请输入[1-7]的整数数字:4
今天是工作日哦!
*/

循环语句

循环语句是由循环体及循环的终止条件两部分组成的。

for语句

  • for语句执行顺序:初始表达式、布尔表达式、循环体、更新表达式。
  • 对于 for 循环,初始化表达式、布尔表达式、更新表达式都不是必须的。
  • 在 for 循环中声明的变量只在for循环中有效。

for语句的语法格式:

for(初始循环表达式;布尔表达式;更新表达式){
	循环体;
}
public class Test {
    public static void main(String[] args) {//计算1~100所有奇数之和
        int sum = 0;
        for(int i = 1; i <= 100; i++){
            if(i % 2 != 0){
                sum += i;
            }
        }
        System.out.println("sum = " + sum);//sum = 2500
    }
}

while语句

while语句的语法格式:

while(布尔表达式){
	循环体;
}
public class Test {
    public static void main(String[] args) {//计算1~100所有偶数之和
        int sum = 0;
        int i = 0;
        while(i <= 100){ 
            if(i % 2 == 0){
                sum += i; 
            }
        i++; 
        }
        System.out.println("sum = " + sum);//sum = 2550
    }
}

do...while语句

  • do…while 循环至少会执行一次。

do...while语句的语法格式:

do{
	循环体;
}while(布尔表达式);
import java.util.Scanner;
public class Test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String username;
        String password;
        do{
            System.out.print("用户名:");
            username = scanner.next();
            System.out.print("密码:");
            password = scanner.next();
        }while(!username.equals("admin") || !password.equals("123"));
        System.out.println("登录成功,欢迎" + username + "回来!");
    }
}

/*
用户名:1
密码:1
用户名:admin
密码:123
登录成功,欢迎admin回来!
*/

转向语句

  • 在Java中转向语句主要有break和continue。
  • 转向语句用于实现循环执行过程中程序流程的跳转。

break语句

  • break语句可以用于switch中和循环中,在循环中,break用来终止跳出循环。
  • break 语句默认情况下只能跳出当前所在的“最近”的“一层”循环。
public class Test {
    public static void main(String[] args) {
        for(int i = 1; i <= 10; i++){
            if(i == 5){
                break;
            }
        System.out.println("break : i = " + i);
        }
    }
}

/*
break : i = 1
break : i = 2
break : i = 3
break : i = 4
*/

break 语句也可以用来终止指定的循环。


public class Test {
    public static void main(String[] args){
        /*
        * 当多层循环嵌套的时候,可以给每个循环设置标识,
        * 例如:first:for...、
        * 当某个条件成立时,想终止指定循环的话,
        * 可以这样做:break first;
        * 这样指定的循环就结束了
        * */
        first:for(int j = 1; j <= 2; j++){
            for(int i = 1; i <= 10 ; i++){
                if(i == 5){
                    break first;
                }
                System.out.println("i = " + i);
            } }
    }
}
/*
i = 1
i = 2
i = 3
i = 4
*/

continue语句

continue 适用于任何循环控制结构中,作用是让程序跳出本次循环,立刻跳入下一次循环。

  • 在 for 循环中:continue 语句使程序跳到更新语句。
  • 在 while 或 do…while 循环中:程序立即跳转到布尔表达式的判断语句。
  • continue语句也存在类似break first;的机制。
public class Test {
    public static void main(String[] args) {
        for(int i = 1; i <= 10; i++){
            if(i == 5){
                continue;
            }
        System.out.println("continue : i = " + i);
        }
    }
}

/*
continue : i = 1
continue : i = 2
continue : i = 3
continue : i = 4
continue : i = 6
continue : i = 7
continue : i = 8
continue : i = 9
continue : i = 10
*/

方法

方法是指一段可以完成某个特定功能的并且可以被重复使用的代码片段。

方法的声明

  • 在使用方法前必须对其声明。
  • 方法的定义需要在类中,其他方法外。
  • 方法不调用,自己不执行。
  • 方法中不能声明方法, 但是,方法中可以调用方法。
  • 如果方法没有明确的返回值类型,使用'空'类型, void表示。void只能在方法返回值类型位置使用,不能作为普通的数据类型使用。
  • 如果方法返回值类型为void类型,可以省略 return ;

方法的声明格式:

[修饰符列表] 返回值列表 方法名(形式参数列表){
	方法体;
}
public static void sumInt(int a,int b){
	int c = a + b;
	System.out.println(a + "+" + b+ "="  +c);
}
/*
*public static 是修饰符列表
*void 是返回值
*sumInt 是方法名
*(int a,int b)是形式参数,每一个形参都是局部变量
*一对大括号括起来的是方法体
*/
  • [修饰符列表],此选项是可选项,非必须的。

  • 返回值,类型可以是Java中任意一种数据类型。如果一个方法执行结束之后不准备返回任何数据,则返回值类型必须写void。

    • 如果方法返回类型不为空,那必须在方法体最后加return 值,且要考虑到所有程序出口的return 值
    • 返回值类型为void,在方法体最后不能加return 值,但是可以加return;
  • 方法名,必须为合法的标识符,遵守驼峰命名。首字母小写,后面每个单词首字母大写。

  • 形式参数,个数为0~N。多个参数用逗号,隔开。形参都是局部变量。形参中起决定性作用的是参数的数据类型,参数名是可以修改的。

  • 方法体,方法体内代码只能遵循自上而下的顺序依次逐行执行。在执行过程中如果需要外部提供数据,则通过形参进行获取。

方法的调用

  • 方法声明完后,需要手动调用。
  • 调用方法的语法格式(前提:方法被static修饰)是“类名.方法名(实际参数列表);”。
  • 调用同类的方法时,可以省略类名;调用其他类的方法时,必须有类名。
  • 实际参数和形式参数的个数和类型必须一一对应。(也可能存在自动类型转换,小转大可以)。
public class Test{
	public static void main(String[] args){
		sumInt(100,200);//100和200为实际参数,类名可省略
		Other.doOther();//类名不可省略!
	}
	public static void sumInt(int x,int y){//int x和int y为形式参数
		System.out.println(x+"+"+y+"="+(x+y));
	}
}
public class Other{
	public static void doOther(){
		System.out.println("Other");
	}
}

关于return关键字

  • return表示结束当前方法。执行return后的本方法内语句是无法执行的。

  • 在同一个域中,return 值return;语句后面是不能写任何代码的,编译报错,因为它无法执行到。

public static int method(boolean flag){//该方法仅示例,返回值为int会报错 
    if(flag){ 
        return 1; 
        System.out.println("第1行"); //没有执行机会
    } 
    System.out.println("第2行"); //当flag=False时,该句有执行机会
    return 0; 
    System.out.println("第3行"); //没有执行机会
}

其中输出“第1行”和“第3行”的代码没有执行机会,编译报错。但“第2行”是有可能执行的。如果声明的时候返回值类型为void,说明该方法没有返回值,那么方法结束的时候就不能编写return 值;这样的语句,但是可以写return;

public static void method(){ 
    return; 
}

return;的作用主要是用来终止方法的执行。当一个方法的返回值类型是void的时候,在方法执行过程中,如果想终止这个方法的执行,执行return;就行。但是要注意break;return;的区别,break用来终止循环,return用来终止一个方法的执行。

//break,跳出循环,循环外的语句仍然执行
public static void main(String[] args) { 
    for(int i = 1; i <= 10; i++){ 
        if(i == 5){ 
            break; 
        } 
    System.out.println("i = " + i); 
} 
    System.out.println("hello world!"); 
}
/*
i = 1
i = 2
i = 3
i = 4
hello world!
*/
//return,跳出方法,方法中return之后的语句都不执行
public static void main(String[] args) { 
    for(int i = 1; i <= 10; i++){ 
        if(i == 5){ 
            return; 
        } 
    System.out.println("i = " + i); 
} 
    System.out.println("hello world!"); //不执行
}
/*

i = 1
i = 2
i = 3
i = 4
*/

方法的重载

方法重载是指同一个类中定义多个同名的方法,但是每个方法具有不同的个数、类型或顺序的参数。

  • 继承父类的子类中有和父类中方法同名,但参数不同的也是方法重载。
  • 方法重载常用于创建完成一组任务相似,但参数个数、类型或顺序不同的方法。
  • 当方法功能不相似时,不能使用方法重载。
public class Test{
    public static void main(String[] args){
        System.out.println(sum(10,20)); //参数类型为整int型
        System.out.println(sum(10L,20L)); //参数类型为long型
        System.out.println(sum(10.0,20.0)); //参数类型为double型
    }
    //定义一个计算int类型数据的求和方法
    public static int sum(int a, int b){
        System.out.println("int求和");
        return a+b;
    }
    //定义一个计算long类型数据的求和方法
    public static long sum(long a, long b){
        System.out.println("long求和");
        return a+b;
    }
    //定义一个计算double类型数据的求和方法
    public static double sum(double a, double b){
        System.out.println("double求和");
        return a+b;
    }
}
  • 在调用时通过传递给他们不同个数、类型或顺序的实参来判断使用哪个方法。

  • 方法重载与修饰词列表和返回值类型无关。

  • 方法重载与形参的变量名无关。

  • 构成重载的条件:

    • 在同一个类中
    • 方法名相同
    • 参数列表不同(个数不同 或 类型不同 或 顺序不同)

参数传递

定义方法时,参数列表中的变量为形式参数。

调用方法时,传入给方法的数值为实际参数。

Java中只有值传递,参数传递的是参数的拷贝,这个拷贝在基本数据类型就是数值,对象类型就是引用。

  • 值传递:适用于基本数据类型和不可变类(String类,基本类型包装类型是引用类型,但是遵循值传递规则),传递的是数据的拷贝,被调用对象对新数据的改变不影响原数据的取值。
  • 引用数据类型:传递的是引用地址的拷贝,被调用对象对新数据的改变影响原数据的取值,因为新数据和原数据的引用虽然不同但却指向堆中的同一数据对象。
public class Test {
    public static void main(String[] args){
        int a = 5;
        int b = 10;
        add(a,b);
        System.out.println(a);//5
        System.out.println(b);//10

        int arr[] = {1,2,3};
        change(arr);
        System.out.println(arr[0]);//2
        System.out.println(arr[1]);//4
        System.out.println(arr[2]);//6

	String str = "Hello!";
        change(str);
        System.out.println(str);//Hello!
    }
    private static void add(int a, int b) {
        a = 100;
        b = 200;
    }
    private static void change(int arr[]) {
        for (int i = 0;i < arr.length;i++){
            arr[i] = arr[i] * 2;
        }
    }
    private static void change(String str) {
        str = "World!";
    }

}

JVM内存结构

  • 方法区:

    • 存储代码片段
    • 存储xxx.class字节码文件
    • 这个空间时最先有数据的,类加载器首先将代码加载到这里
  • 堆内存:

    • 后面讲(面向对象)
  • 栈内存:

    • 每个方法执行时所需的内存空间(局部变量)

image.png

栈****数据结构

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是:仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。遵循后进先出/先进后出的原则。 向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。 从一个栈删除元素又称作出栈、退栈或弹栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

调用方法时所需的内存空间在栈中分配,根据栈先进后出的原则,最先调用的方法一定是最后结束的。

image.png

执行方法时的内存变化

方法调用时:压栈。

方法执行结束时:弹栈。

public class Test {

    public static void main(String[] args) {
        System.out.println("main begin"); 
        m1(); 
        System.out.println("main over"); 
    } 
    public static void m1() {
        System.out.println("m1 begin"); 
        m2(); 
        System.out.println("m1 over"); 
    } 
    public static void m2() {
        System.out.println("m2 begin"); 
        System.out.println("m2 over"); 
    } 
}
/**
运行结果为
main begin
m1 begin
m2 begin
m2 over
m1 over
main over
*/

通过上面的执行结果可以了解到,main方法最先被调用,但它是最后结束的;m2方法最后被调用,但它是最先结束的(提醒:调用的时候分配内存是压栈,结束的时候是释放内存弹栈)。内存变化如下图:

image.png

方法的递归

方法调用自身叫做方法的递归调用。

  • 当递归时,程序没有结束条件会导致栈内存溢出错误(StackOverflowError)。JVM发生错误后只有一个结果,就是退出JVM。
  • 当递归有结束条件时也有可能出现栈内存溢出错误,因为有可能递归太深,栈内存不够了,因为一直在压栈。

image.png

注:实际开发中避免轻易使用递归,尽量使用循环代替递归,因为循环的效率高、占用内存少,且递归使用不当会导致JVM死掉。但在极少数情况下,不用递归程序无法实现。

public class Test {
    public static void main(String []args){//使用递归的方式计算5的阶乘
     int n = 5;
     int result = f(n);
     System.out.println(result);
    }
    public static int f(int n){
        if (n == 1){
            return 1;
        }
        return n*f(n-1);
    }
}
public class Test {
    public static void main(String[] args){//使用递归的方式计算1加到n的和
        int n=3;
        System.out.println(sum(n));
    }
    public static int sum(int n){
        //如果n是1,那么1到n的和就是1
        if(n==1){
            return 1;
        }
        //如果n不是1,那么要将n-1后再与n相加
        return n + sum(n-1);
    }
}

image.png

遇到StackOverflowError错误时,如何解决?

  1. 先检查递归结束条件对不对,若不对先对条件进行修改;
  2. 若条件没问题,需要手动调整JVM栈内存初始化大小,可以将栈内存的空间调大点;
  3. 若调整栈内存初始大小后还是出现这个错误,智能继续扩大栈的内存大小。

image.png

面向对象

面向对象的三大特征

封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。

类和对象介绍

类是现实世界中具有共同特征的事物进行抽象形成的模板或概念。
对象是实际存在的个体。

  • 通过类可以创建对象,对象也被称为实例,创建对象的过程也叫实例化。
  • 对对象的特征进行抽取形成类,这个过程叫抽象。

类的定义

类由属性和方法构成。

  • 属性描述的是状态,方法描述的是行为动作。
  • 对象的属性以变量的形式存在,这里的变量就是成员变量中的实例(成员)变量。
  • 在Java中,凡是使用class创建的类,都属于引用数据类型。

定义类的语法格式:

[修饰符] class 类名 {
	类体;//类体=属性+方法
}
public class Student{//定义一个学生类
	//学号
	int no;
	//姓名
	String name;
	//年龄
	int age;
	//性别
	boolean sex;
}

对象的创建和使用

对象的创建

  • 类定义后才可以创建对象。
  • 一个类可以创建多个对象。

创建对象的语法格式:

new 类名();
public class Test{
	public static void main(String[] args){
		//创建一个学生对象
		new Student();
		//再创建两个学生对象,用变量接收一下
		Student s1 = new Student();
		Student s2 = new Student();
	}
}
public class Student{//定义一个学生类
	//学号
	int no;
	//姓名
	String name;
	//年龄
	int age;
	//性别
	boolean sex;
}

对象的使用

public class Test{
	public static void main(String[] args){
		//创建一个学生对象
		Student s = new Student();
		//读取属性变量值
		System.out.println("学号:" + s.no);//学号:0
		System.out.println("姓名:" + s.name);//姓名:null
		System.out.println("年龄:" + s.age);//年龄:0
		s.sex = true;//修改属性变量值
		System.out.println("性别:" + s.sex);//性别:true
	}
}
public class Student{//定义一个学生类
	//学号
	int no;
	//姓名
	String name;
	//年龄
	int age;
	//性别
	boolean sex;
}
  • 如果实例变量没有被手动赋值,在创建对象时,程序会给实例变量默认赋值。引用类型的默认是是null。

构造方法

构造方法是类中的特殊方法。

  • 通过调用构造方法来完成对象的创建以及对象属性的初始化操作。

构造方法定义的语法格式:

[修饰符列表] 构造方法名(形式参数列表){
	构造方法体;
}
  • 构造方法名和类名一致。
  • 构造方法不能写返回值类型(包括void)。
  • 一个类中可以定义多个构造方法,即允许方法重载。
  • 构造方法的返回值是当前类的类型。
  • 当一个类没有显示定义任何构造方法时,程序会默认提供无参构造方法。
  • 一旦有程序员自己定义的构造方法后,程序将不再默认提供无参构造方法。
public class Test {
    public static void main(String[] args){
        new Data();
        new Data(2020);//没有手动赋值的属性,程序依然会自动默认
        new Data(2020,8);
        new Data(2020,8,23);
    }
}

/*
0,0,0 无参构造
2020,0,0 带year构造
2020,8,0 带year,month构造
2020,8,23 带year,month,day构造
*/
public class Data {
    int year;
    int month;
    int day;

    public Data(){
        System.out.println(year+","+month+","+day+" 无参构造");
    }
    public Data(int year){
        System.out.println(year+","+month+","+day+" 带year构造");
    }
    public Data(int year,int month){
        System.out.println(year+","+month+","+day+" 带year,month构造");
    }
    public Data(int year,int month,int day){
        System.out.println(year+","+month+","+day+" 带year,month,day构造");
    }
}

空指针异常

public class PointerTest{
	public static void main(String[] args){
		Pointer p = new Pointer();
		System.out.println(p.i);
		p = Null;//造成空指针异常java.lang.NullPointerException,报错!
		System.out.println(p.i);
	}
} 

/*
0
Exception in thread "main" java.lang.NullPointerException
at PointerTest.main(PointerTest.java:6)
*/
public class Pointer{
	int i;
}
  • 当p=null时,p引用就无法找到堆内存中的Java对象了,程序就无法正常访问。

方法调用时参数的传递问题

当参数为基本数据类型时,参数传递的是数据 10:

public class Test {
    public static void main(String[] args){
        int x = 10;
        int y = x;//仅将x的值复制一份给y
        add(x);
        System.out.println("main:"+y);
    }
    public static void add(int i){
        i++;
        System.out.println("add:"+i);
    }
}

/*
add:11
main:10
*/

当参数为引用数据类型时,参数传递的是内存地址:

public class Test {
    public static void main(String[] args){
        Person p = new Person();
        Person p1 = new Person();
        p.age = 10;
        p1 = p;//将p的地址复制一份给p1
        add(p1);//因为p1和p的内存地址相同,因此修改p1中age就相当于修改p中的age
        System.out.println("main:"+p.age);
    }
    public static void add(Person p2){
        p2.age++;
        System.out.println("add:"+p2.age);
    }
}
class Person{
    int age;
}

/*
add:11
main:11
*/
  • 前者把x的值“10”复制一份给了y,而后者把p的内存地址复制了一份给了p1,因此导致p和p1指向的地址是同一个Java对象,通过任何一个引用去访问堆内存中的对象,对象内存都会受到影响。

封装

封装是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能的隐藏内部的细节,只保留一些对外接口使之与外部发生联系。

  • 系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。
  • 封装降低了程序的耦合度,提高了系统的扩展性以及重用性,可以隐藏内部实现细节,外部看不到内部复杂结构,对外只提供简单的安全的操作入口,更安全。

如何封装

  • 使用Java语言中的private修饰符,private表示私有的,私有的数据只能在本类中访问。
public class MobilePhoneTest {
    public static void main(String[] args){
        MobilePhone phone = new MobilePhone();
        phone.voltage = 3.7;
        System.out.println(phone.voltage);
        phone.voltage = 100;
        System.out.println(phone.voltage);
    }
}

/*
java: voltage 在 MobilePhone 中是 private 访问控制 以上访问修改不了
*/
public class MobilePhone {
    private double voltage;
}
  • private修饰的数据无法在外部程序直接访问。

set和get方法

set和get方法是用来提供访问入口的。

  • set和get方法访问的都是某个具体对象的属性,不同对象调用set和get方法获取的属性值不同,set和get方法必须有对象的存在才能调用。
  • set方法是用来修改属性值的。一般在set方法内部编写安全控制程序。
  • get方法是用来读取属性值的。不需要参数。返回值类型是该属性所属类型。

set方法定义的语法格式:

public void set+属性名首字母大写(参数){
	xxx = 参数;
}

get方法定义的语法格式:

public 返回值类型 get+属性名首字母大写(无参数){
	return xxx;
}
public class MobilePhoneTest {
    public static void main(String[] args){
        MobilePhone phone = new MobilePhone();
        phone.setVoltage(3.7);
        System.out.println(phone.getVoltage());
        phone.setVoltage(100);
        System.out.println(phone.getVoltage());
    }
}

/*
3.7
Exception in thread "main" java.lang.RuntimeException: 电压输入错误
at MobilePhone.setVoltage(MobilePhone.java:10)
at MobilePhoneTest.main(MobilePhoneTest.java:6)
*/
public class MobilePhone {

    private double voltage;
    public MobilePhone(){//无参构造方法

    }
    public void setVoltage(double _voltage) {//set方法,只能用来修改属性值
        if(_voltage < 3|| _voltage > 5){//用来设置门槛,保证修改安全
            throw new RuntimeException("电压输入错误");
        }
        voltage=_voltage;
    }
    public double getVoltage() {//get方法,只能用来读取数据
        return voltage;
    }
}

this和static

this

this是Java中的关键字,指“当前对象”

  • this这个引用保存了当前对象的内存地址指向自身。
  • this在实例方法以及构造方法中,语法格式分别为“this.”和“this(实际参数列表)”。
  • this不能出现在有静态方法中,只能出现在实例方法中。
public class Test {
    public static void main(String[] args){
        Customer c1 = new Customer("张三");
        c1.shopping();
        Customer c2 = new Customer("李四");
        c2.shopping();
    }
}
class Customer{
    String name;
    public Customer(){
    }
    public Customer(String s){
        name=s;
    }
    public void shopping(){
        System.out.println(this.name + "正在购物!");//name前面可以隐藏(this.)
    }
}
  • 当形参名和类中属性名重名时,this是不能省略的。
class Customer{
    String name;
    public Customer(){
    }
    public Customer(String name){
        //name = name;//如果形参也命名为name,系统会遵循就近原则,所以name = name中,两个name都为形参name
        this.name = name;//这是this的第二个用法,前者是本类中实例变量name,后者是形参name,此处不能省略this
    }
  • this(实际参数列表)用在构造方法里
public class Date {
    private int year;
    private int month;
    private int day;
    public Date(){
        this.year = 1970;
        this.month = 1;
        this.day = 1;
    }
    public Date(int year,int month,int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public int getYear(){
        return year;
    }
    public void setYear(int year){
        this.year = year;
    }
    public int getMonth(){
        return month;
    }
    public void setMonth(int month){
        this.month = month;
    }
    public int getDay(){
        return day;
    }
    public void setDay(int day){
        this.day = day;
    }
}

public class DateTest {
    public static void main(String[] args){
        Date d1 = new Date();
        System.out.println(d1.getYear()  +  "年 "  +  d1.getMonth()  +  "月 "  + d1.getDay() + "日");
        Date d2 = new Date(2008,8,8);
        System.out.println(d2.getYear()  +  "年 "  +  d2.getMonth()  +  "月 "  + d2.getDay() + "日");
    }
}

在上面的无参构造方法和有参构造方法中,代码基本相同,没有复用,这时可以采用this()的方式。

  • this()语法只能出现在构造方法的第一行,因此在一个构造方法中this()只能出现一次。
public Date(){
    this(1970,1,1);
}
public Date(int year,int month,int day){
    this.year = year;
    this.month = month;
    this.day = day;
}

static

static是Java中的关键字,表示“静态的”,可以用来修饰变量、方法、代码块等。

  • 在Java中凡是用static修饰的都是类相关的。
  • 实例变量必须先创建对象才能访问,通过“引用.”访问。实例变量在构造方法执行时才初始化。
  • 静态变量访问时不需要创建对象,直接通过“类名.”访问。静态变量在类加载时就初始化。

static变量

当一个类中所有对象的某个属性值不会随着对象的变化而变化时,我们可以把这个属性定义为静态属性。

  • 静态属性不需要创建对象,在类加载时初始化,直接通过“类名.”来访问。
  • 引用.的形式也可以,但是很不建议用,实际上程序还是通过“类.”的方式访问的,并不会出现空指针异常。

实例:

public class ManTest{
	public static void main(String[] args){
		System.out.println(Man.sex);//静态变量用类名.形式访问
	}
}
public class Man{
	int idCard;
	static boolean sex = true;
}

static方法

当方法体中需要直接访问当前对象的实例变量或者实例方法的时候,该方法必须定义为实例方法。

  • 静态方法不需要创建对象,直接通过“类名.”访问。也可以用使用“引用.”来访问,但是很不建议
  • 方法描述的是行为动作,当某个行为动作需要对象的参与,这个方法应该定义为实例方法。
  • 当方法体中直接访问了实例变量,那这个方法一定是实例方法。
  • “工具类”中的方法一般定义为静态方法。

比如为了简化输出语句编写的工具类

public class U {
	public static void p(int data){ 
		System.out.println(data);
	}
	public static void p(long data){ 
		System.out.println(data);
	}
	public static void p(float data){ 
		System.out.println(data);
	}
	public static void p(double data){ 
		System.out.println(data);
	}
	public static void p(boolean data){ 
		System.out.println(data);
	}
	public static void p(char data){
		System.out.println(data); 
	}
	public static void p(String data){ 
	System.out.println(data);
	}
}
public class HelloWorld {
	public static void main(String[] args) { 
		U.p("Hello World!");
		U.p(true); 
		U.p(3.14); 
		U.p('A');
	} 
}

静态代码块

静态代码块是Java为程序员提供的一个特殊时刻,如果想在类加载时,执行一段代码就使用静态代码块。

  • 静态代码块在类加载时执行,在main方法执行前,并且只执行一次。

静态代码块的语法格式:

类{
    static{
   	java语句;
    }
}
  • 静态代码块内部和静态代码块之间都遵循自上而下的顺序执行,类体内代码也有执行顺序。
public static StaticTest{
	static int i = 100;//放在此处正确
	static{
		System.out.println(i);
	}
	static int i = 100;//放在此处访问不到
}
//静态代码块是访问不到后面的i变量的,必须将变量放在前面

实例代码块

实例语句块在构造方法执行前自动执行。

实例代码块的语法格式:

类{
    {
    	java语句;
    }
}

静态代码块和实例代码块的执行顺序

public class CodeOrder {
    static{
        System.out.println("A");
    }
    public static void main(String[] args){
        System.out.println("Y");
        new CodeOrder();
        System.out.println("Z");
    }
    public CodeOrder(){
        System.out.println("B");
    }
    {
        System.out.println("C");
    }
    static{
        System.out.println("X");
    }

}

/*
A
X
Y
C
B
Z
*/

继承

子类继承父类的特征和行为,使子类对象(实例)具有父类的属性。

  • 继承可以看作将父类中的属性方法(除构造方法外)直接复制到子类中。
  • 不同的类中持有的共同的特征和动作,可以放在一个通用类中,让其他类共享。

Java中继承的语法格式:

class 类名 extends 父类名{
     类体;
}
public class Account { //银行账户类 
	//账号
	private String actno; //余额
	private double balance;
	//账号和余额的 set 和 get 方法 
	public String getActno() {
		return actno;
	}
	public void setActno(String actno) { 
		this.actno = actno;
	}
	public double getBalance() {
		return balance; 
	}
	public void setBalance(double balance) { 				
		this.balance = balance;
	}
}
public class CreditAccount extends Account{ //信用账户类 
	//信誉度(信用账户类特有属性)
	private double credit; 
	//信誉度的 set 和 get 方法 
	public double getCredit() {
		return credit; 
	}
	public void setCredit(double credit) { 
		this.credit = credit;
	} 
}
public class AccountTest {
	public static void main(String[] args) { 
		CreditAccount act = new CreditAccount(); act.setActno("111111111");
		act.setBalance(9000.0);
		System.out.println(act.getActno()+"信用账户,余额"+act.getBanlance()+"元");
	}
}
  • 通过继承解决了代码复用的问题,但继承会导致耦合度高。
  • B 类继承 A类,称 A类为超类、父类、基类,B 类则称为子类、派生类、扩展类。
  • java 中的继承只支持单继承,不支持多继承。但可以通过间接继承,达到多继承的效果。
  • 子类继承父类,构造方法和被 private 修饰的数据不能被继承。
  • java 中的类若没有显性继承任何类,则默认继承根类Object。

方法的覆盖和多态

方法覆盖

  • 父类继承过来的方法无法满足子类业务需求时,可以将父类中继承过来的方法重写。
  • 当该方法被重写后,子类对象一定会调用重写后的方法。
public class Animal {
    public void move(){
        System.out.println("动物在移动!");
    }
}
public class Cat extends Animal{
    public void move(){
        System.out.println("猫在走猫步!");
    }
    public void catchMouse(){
        System.out.println("猫在抓老鼠");
    }
}

public class Bird extends Animal{
    public void move(){
        System.out.println("鸟儿在飞翔!");
    }
}

public class Test {
    public static void main(String[] args){
        Cat cat = new Cat();
        cat.move();
        cat.catchMouse();
        Bird bird = new Bird();
        bird.move();
    }
}

/*
猫在走猫步!
猫在抓老鼠
鸟儿在飞翔!
*/
  • 方法覆盖只能用在具有继承关系的父子类之间。
  • 覆盖后的方法应与原方法具有相同的返回值,方法名和相同的形式参数列表(建议直接复制到子类)。
  • 构造方法和私有的方法不能被继承,所以不能被覆盖。
  • 覆盖后的方法不能比原方法有更低的访问权限,只能更高(封闭)。
  • 覆盖后的方法不能比原方法抛出更多的异常,可以相同或少。
  • 方法覆盖只和方法有关,与属性无关。
  • 静态方法不存在覆盖(可以但意义不大)。

多态

多态就是同一个行为发生在不同的对象上会产生不同的效果。

  • Java中允许多态两种语法出现:向上转型和向下转型。
  • 无论是向上转型还是向下转型,两个类必须有继承关系。

向上转型

向上转型是子类型转为父类型。又称为自动转换类型。

public class PolyTest {
    public static void main(String[] args) { //创建 Animal 对象
        Animal a1 = new Cat();//将Animal类属性指向Cat类
        a1.move();
        Animal a2 = new Bird();
        a2.move();
    }
}
class Animal {
    public void move(){
        System.out.println("Animal move!");
    }
}
class Cat extends Animal{
    //方法覆盖
    public void move(){
        System.out.println("走猫步!");
    }
    //子类特有
    public void catchMouse(){
        System.out.println("抓老鼠!");
    }
}
class Bird extends Animal { //方法覆盖
    public void move() {
        System.out.println("鸟儿在飞翔!");
    }
    //子类特有
    public void sing() {
        System.out.println("鸟儿在歌唱!");
    }
}
/*
走猫步!
鸟儿在飞翔!
*/

Java程序运行包括编译和运行:

  • 在编译阶段编译器只知道a1的数据类型是Animal,编译器会去Animal.class字节码中查找move()方法,发现方法后,将move()方法绑定到a1引用上,编译通过。这叫静态绑定。
  • 在运行阶段实际上在堆内存中存的是Cat类型,所以运行时会自动执行Cat类中的move()方法。这叫动态绑定。

向下转型

向下转型是父类型转为子类型。又称为强制转换类型。

  • 当需要访问子类对象中的特有方法时,需要向下转型。
public class PolyTest {
    public static void main(String[] args) {
        Animal a = new Cat();
        a.catchMouse();//访问子类中特有方法
    }
}
class Animal {
    public void move(){
        System.out.println("Animal move!");
    }
}
class Cat extends Animal{
    //方法覆盖
    public void move(){
        System.out.println("走猫步!");
    }
    //子类特有
    public void catchMouse(){
        System.out.println("抓老鼠!");
    }
}
class Bird extends Animal { //方法覆盖
    public void move() {
        System.out.println("鸟儿在飞翔!");
    }
    //子类特有
    public void sing() {
        System.out.println("鸟儿在歌唱!");
    }
}
/*
java: 找不到符号
      符号:   方法 catchMouse()
      位置: 类型为Animal的变量 a
*/
  • 在编译时编译器只知道a变量的数据类型是Animal,只会去Animal.class字节码文件中寻找catchMouse()方法,但是没找到,编译错误。

代码应改为:

public class PolyTest {
    public static void main(String[] args) {
        Animal a = new Cat();
        Cat c = (Cat)a;//强制转换类型:将Animal强转换成Cat类
        c.catchMouse();
    }
}
class Animal {
    public void move(){
        System.out.println("Animal move!");
    }
}
class Cat extends Animal{
    //方法覆盖
    public void move(){
        System.out.println("走猫步!");
    }
    //子类特有
    public void catchMouse(){
        System.out.println("抓老鼠!");
    }
}
class Bird extends Animal { //方法覆盖
    public void move() {
        System.out.println("鸟儿在飞翔!");
    }
    //子类特有
    public void sing() {
        System.out.println("鸟儿在歌唱!");
    }
}
  • 向下转型有风险
public class PolyTest {
    public static void main(String[] args) {
        Animal a = new Bird();
        Cat c = (Cat)a;
    }
}
class Animal {
    public void move(){
        System.out.println("Animal move!");
    }
}
class Cat extends Animal{
    //方法覆盖
    public void move(){
        System.out.println("走猫步!");
    }
    //子类特有
    public void catchMouse(){
        System.out.println("抓老鼠!");
    }
}
class Bird extends Animal { //方法覆盖
    public void move() {
        System.out.println("鸟儿在飞翔!");
    }
    //子类特有
    public void sing() {
        System.out.println("鸟儿在歌唱!");
    }
}
/*
Exception in thread "main" java.lang.ClassCastException: class Bird cannot be cast to class Cat (Bird and Cat are in unnamed module of loader 'app')
    at PolyTest04.main(PolyTest04.java:4)
*/
  • 编译可以通过,因为编译器只知道在a变量是Animal类且存在继承关系,语法上没有错误,编译通过。
  • 运行没有通过,因为在运行时堆内存中创建了一个Bird对象,Cat和Bird是没有继承关系的,所以强转失败。

instanceof运算符

在向下转型时,需要用instanceof来判断是否可以进行强转。

  • instanceof运算符的结果是布尔类型。
  • (c instanceof Cat)结果是true,说明在运行阶段c引用指向的对象是Cat类型。
public class PolyTest {
	public static void main(String[] args) { 
		Animal a = new Bird();
		if(a instanceof Cat){ 
			Cat c = (Cat)a; 
			c.catchMouse();
		} 
	}
}
class Animal {
    public void move(){
        System.out.println("Animal move!");
    }
}
class Cat extends Animal{
    //方法覆盖
    public void move(){
        System.out.println("走猫步!");
    }
    //子类特有
    public void catchMouse(){
        System.out.println("抓老鼠!");
    }
}
class Bird extends Animal { //方法覆盖
    public void move() {
        System.out.println("鸟儿在飞翔!");
    }
    //子类特有
    public void sing() {
        System.out.println("鸟儿在歌唱!");
    }
}

多态在开发中的应用

public class PolyTest {
    public static void main(String[] args){
        Dog dog = new Dog("二哈");
        Bird bird = new Bird("小鸟儿");
        Master master = new Master();
        master.feed(dog);
        master.feed(bird);
    }
}
class Master{
    public void feed(Pet pet){
        System.out.println("主人开始喂食");
        pet.eat();
        System.out.println("主人喂食完毕");
    }
}
class Pet{
    String name;
    public void eat(){

    }
}
class Dog extends Pet{
    public Dog(String name){//有参构造方法
        this.name = name;
    }
    public void eat(){//重写方法
        System.out.println(this.name+"在吃骨头");
    }
}
class Bird extends Pet{
    public Bird(String name){//有参构造方法
        this.name = name;
    }
    public void eat(){//重写方法
        System.out.println(this.name+"在唱歌");
    }
}
/*
主人开始喂食
二哈在吃骨头
主人喂食完毕
主人开始喂食
小鸟儿在唱歌
主人喂食完毕
*/

super关键字

super代表当前对象中从父类继承过来的那部分特征。

  • super()只能从出现在构造方法第一行,通过当前方法去调用父类中的构造方法,目的是创建子类对象时,先初始化父类特征。
  • super可以出现在实例方法和构造方法中,但不能使用在静态方法中。
  • super的语法为:"super"、"super()"。
  • super. 大部分情况下不能省略,例如:父类子类用同名属性,或同名方法。想在子类中访问父类的不能省略。
  • super和this不能共存。
  • 当一个构造方法第一行:既没有this()有没有super(),默认会有一个super();表示通过当前子类构造方法调用父类的无参构造方法。
  • super不是引用,也不保存内存地址,也不指向任何对象。
public class SuperTest {
    public static void main(String[] args){
        new B();
    }
}
class A{
    public A(){
        System.out.println("A类型的无参构造方法");
    }
    public A(int i){
        System.out.println("A类型的有参构造方法");
    }
}
class B extends A{
    public B(){
        super();/如果此处没写任何东西,默认会有一个super();去调用父类的无参构造方法
        //super(100);如果此处写了super(100);那只会调用父类的有参构造方法
        System.out.println("B类型的无参构造方法");
    }

}
/*
A类型的无参构造方法
B类型的无参构造方法
*/
public class SuperTest {
    public static void main(String[] args){
        new C();
    }
}
class A{
    public A(){//无参构造
        System.out.println("1");
    }
}
class B extends A{
    public B(){//无参构造
        System.out.println("2");
    }
    public B(String name){//有参构造
        //此处存在隐式无参super
        System.out.println("3");
    }
}
class C extends B{
    public C(){//无参构造
        this("zhangsan");
        System.out.println("4");
    }
    public C(String name){//有参构造
        this(name,20);
        System.out.println("5");
    }
    public C(String name,int age){//有参构造
        super(name);//显式有参super
        System.out.println("6");
    }
}
/*
1
3
6
5
4
*/

super(实参列表)的适当使用:

public class SuperTest {
    public static void main(String[] args){
        CreditAccount ca1 = new CreditAccount();
        System.out.println(ca1.getActno()+","+ca1.getBalance()+","+ca1.getCredit());
        CreditAccount ca2 = new CreditAccount("1111",1000.0,0.99);
        System.out.println(ca2.getActno()+","+ca2.getBalance()+","+ca2.getCredit());
    }
}
class Account{
    //账户号
    private String actno;
    //余额
    private double balance;
    public Account(){

    }
    public Account(String actno,double balance) {
        this.actno = actno;
        this.balance = balance;
    }
    public void setActno(){
        this.actno = actno;
    }
    public String getActno(){
        return actno;
    }
    public void setBalance(double balance){
        this.balance = balance;
    }
    public double getBalance(){
        return balance;
    }
}
class CreditAccount extends Account{
    //属性:信誉度,子类特有的特征
    private double credit;
    public CreditAccount(){

    }
    public CreditAccount(String actno,double balance,double credit){
        //this.actno = actno;
        //this.balance = balance;
        // 这两条的属性是父类中的私有属性,无法访问
        //可以通过super()去调用父类中的有参构造方法
        super(actno,balance);
        this.credit = credit;
    }
    public void setCredit(double credit){
        this.credit = credit;
    }
    public double getCredit(){
        return credit;
    }
}
/*
null,0.0,0.0
1111,1000.0,0.99
*/
  • super什么时候不能省略:当父类子类有同名实例变量或实例方法时。
public class SuperTest {
    public static void main(String[] args){
        Vip v = new Vip("张三");
        v.shopping();
    }
}
class Customer{
    String name;
    public  Customer(){

    }
    public Customer(String name){
        //super();此处省略了
        this.name = name;
    }
}
class Vip extends Customer{
    //和父类有同名属性
    String name;
    public Vip(){

    }
    public Vip(String name){
        super(name);
    }
    public void shopping(){
        System.out.println(this.name + "正在购物");
        System.out.println(super.name + "正在购物");
        System.out.println(name + "正在购物");
    }
}
/*
null正在购物
张三正在购物
null正在购物
*/
  • super不仅可以访问父类属性,也可以访问父类方法。
public class SuperTest {
    public static void main(){
        Cat c = new Cat();
        c.yiDong();
    }
}
class Animal{
    public void move(){
        System.out.println("Animal move");
    }
}
class Cat extends Animal{
    public void move(){
        System.out.println("Cat move");
    }
    public void yiDong(){
        this.move();
        move();
        super.move();
    }
}
/*
Cat move
Cat move
Animal move
*/

使用IDEA集成开发环境

IDEA的使用

  • 在IDEA中,选择 File-New-Project 创建一个工程。
  • 工程命名并为其选择工程目录,点击Finish。
  • 弹出项目结构,点击上面的+号,选择New Module。
  • 选择Java,点击Next,为Module命名,点击Finish,点击OK。
  • 模块创建完成,在模块中的src中新建Java类。

IDEA快捷键

快速生成main方法 psvm或main
快速生成输出语句 sout
删除一行 Ctrl+Y
左侧列表移动开合 方向键
新增、新建、添加的快捷键 Alt+Insert
窗口变大小 Ctrl+Shift+F12
切窗口 Alt+左右箭头
运行 Ctrl+Shift+F10
单行注释 Ctrl+/
多行注释 Ctrl+Shift+/
定位方法属性变量 停到单词下单击Ctrl
纠正错误 Alt+Enter
查看类的属性和方法 Ctrl+F12

面向对象进阶

final关键字

  • final修饰的变量不能被修改。
  • final修饰的变量必须显式初始化。
  • final修饰的方法不能被覆盖。
  • final修饰的类不能被继承。
  • 如果修饰的引用,那这个引用只能指向一个对象,不可重新赋值,但是被指向的对象可以修改。
  • final不能修饰构造方法。

final修饰的(基本数据类型)变量不能被修改

public class Test {
    public static void main(String[] args) {
        final int k = 10;
        k = 20;
    }
}
/*
java: 无法为最终变量k分配值
*/

final修饰的实例变量

  • 被final修饰的实例变量必须手动赋值,不会自动赋默认值。
  • 在构造方法里赋值也行,但是set方法不行。
public class Test {
    public static void main(String[] args) {

    }
}
class Person{
    final int age = 0;//手动赋值
    final boolean sex;//被final修饰的实例变量必须手动赋值,系统不会自动赋默认值
    public Person(){
		this.sex = true;//手动赋值
	}
    public Person(boolean b){
		this.sex = b;//手动赋值
	}
}
  • 实例变量被final关键字修饰后,一般会加static变为静态。
  • static final修饰的变量称为常量,常量不可更改,一般为公开的。
public class Test {
    public static void main(String[] args) {

    }
}
class Chinese{
    String idCard;
    String name;
    static final String COUNTRY = "中国";//常量
}

final修饰的引用变量不能被修改

  • final修饰的引用变量不能被修改,但引用内部的数据是可以被修改的。
public class Test {
    public static void main(String[] args) {
        final Person p = new Person(30);//该引用只能指向一个对象,并且永远只指向这个对象
        p = new Person(30);//p引用保存的地址是不可修改的
        System.out.println(p.age);
        p.age = 40;//引用内部的数据是可以被修改的
        System.out.println(p.age);
    }
}
class Person{
    int age;

    public Person() {
    }
    public Person(int age) {
        this.age = age;
    }
}
/*
java: 无法为最终变量p分配值
*/

final关键字修饰的类不能被继承

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

    }
}
final class A{

}
class B extends A{

}
/*
java: 无法从最终A进行继承
*/

final修饰的方法不能被覆盖重写

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

    }
}
class A{
    public final void doSome(){

    }
}
class B extends A{
    public  void doSome(){

    }
}
/*
java: B中的doSome()无法覆盖A中的doSome()
	  被覆盖的方法为final
*/

抽象类

抽象类是类和类之间有共同特征再抽象成抽象类。

  • 在Java中采用abstract定义的类就是抽象类,采用abstract定义的方法就叫抽象方法。
  • 抽象类属于引用数据类型。
  • 抽象方法只能存在于抽象类中,只声明,不实现。
  • 如果有一个方法为抽象的,那么此类必须为抽象的。如果一个类是抽象的,并不要求具有抽象的方法。
  • 抽象父类中有抽象方法,继承的子类要么也为抽象类,要么在子类中覆盖抽象方法。
  • 抽象类无法实例化,无法创建对象。
  • 抽象类也有构造方法,供子类使用。
  • 抽象类不能用final修饰,抽象类就是用来被继承的。
  • 抽象方法不能被final修饰,抽象方法就是用来被子类实现的。
  • 向上转型多态编译时,会绑定父类中的方法,在执行时会执行子类的。

抽象类的定义

抽象类定义的语法格式:

[修饰符列表] abstract class 类名{
	类体;
}

抽象方法

抽象方法是没有实现、没有方法体的方法。

public abstract void doSome();
public class Test {
    public static void main(String[] args) {
        A a = new A();//报错,抽象类无法实例化
        B b = new B();//非抽象子类可以实例化
    }
}
abstract class A{//在抽象类中可以定义一些子类公共的方法或属性
    public abstract void doSome();
    public void doOther(){//抽象类中可以存在非抽象方法

    }
}
class B extends A{
    public void doSome(){//抽象方法在非抽象子类中必须重写

    }
}
abstract class C extends A{//在抽象子类中不需要重写父类抽象方法
    public abstract void doSome();//再次声明该方法为抽象的
}

接口

接口是抽象类中的一种特殊情况,在接口中只能定义抽象的方法和常量。

接口的基础语法

  • 接口也是一种引用数据类型,接口编译后也是.class字节码文件。
  • 接口是完全抽象的(抽象类是半抽象)。
  • 接口之间可以继承,并且支持多继承。但接口之间不能实现。
  • 接口中只能包含常量和抽象方法(没有构造方法)。
  • 接口不能被实例化,接口没有构造函数的概念。
  • 接口中所有元素都是公开的,public可以省略。
  • 接口中抽象方法默认都是public abstract,可以省略。
  • 接口中变量都是public static final,可以省略,必须显式初始化。
  • 一个非抽象的类,实现接口时,必须将接口中所有方法实现。
  • 一个类可以实现多个接口。
  • extends和implements同时出现时,必须extends写在前,implements写在后。
  • 使用接口时,可以使用多态(父类型引用指向子类型对象)。

定义接口的语法格式:

[修饰符列表] interface 接口名{

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

    }
}
interface A{

}
interface B extends A{

}
interface C extends A,B{

}
interface MyMath{

    //public static final double PI=3.12;
    double PI=3.12;//省略了public static 
    //public abstract int sum(int a,int b);
    int sum(int a,int b);//省略了public static 
}
  • 接口可以看作是类,类之间叫继承,类和接口之间叫实现通过implements关键字完成。
  • 当一个非抽象的类实现接口时,必须将接口中的所有抽象方法全部实现(覆盖重写)。
  • 子类的方法访问权限要更低(封闭)才行。
public class Test {
    public static void main(String[] args) {

    }
}

interface MyMath{
    double PI=3.12;
    int sum(int a,int b);
    int sub(int a,int b);
}
class MyMathImp implements MyMath{//重写的方法访问权限必须更封闭
	public int sum(int a,int b){
	    return a+b;
	}  
	public int sub(int a,int b){
            return a-b;
	}  
}
//abstract class MyMathImp implements MyMath{

//}

接口和多态联合使用

public class Test {
    public static void main(String[] args) {
        MyMath mm = new MyMathImp();//运用了多态机制,用父类接受
        System.out.println(mm.sum(30,10));
        System.out.println(mm.sub(30,10));
    }
}
interface MyMath{
    double PI=3.14;
    int sum(int a,int b);
    int sub(int a,int b);
}
class MyMathImp implements MyMath{
    public int sum(int a,int b){
        return a+b;
    }
    public int sub(int a,int b){
        return a-b;
    }
}
/*
40
20
*/
  • 一个类可以实现多个接口,弥补了类不能多继承的缺点。
  • 接口之间在强制类型转换时,没有继承关系也可以强转。但是运行时可能会出现ClassCastException异常(假装当没讲,容易混淆没啥用)。
public class Test {
    public static void main(String[] args) {
		A a = new D();
		B b = new D();
		C c = new D();

		B b2 = (B)a;//A和B之间没有继承关系
		b2.b();
    }
}

interface A{
    void a();
}
interface B{
    void b();
}
interface C{
    void c();
}
class D implements A,B,C{
    public void a(){

    }
    public void b(){
	System.out.println("b....");
    }
    public void c(){
 
    }
}
/*
b....
*/
public class Test {
    public static void main(String[] args) {
        B b = new C();
       if(B instanceof A){
            A a = (A)b;//如果没有检查,就会报错
	/*Exception in thread "main" java.lang.ClassCastException: class C cannot be cast to class A (C and A are in unnamed module of loader 'app')
	at Test1.main(Test1.java:4)*/
       }
   
    }
}

interface A{

}
interface B{

}
class C implements B{

}
  • extends和implements同时出现,extends写在前,implements写在后。
  • 接口通常提取的是行为动作。
public class Test {
    public static void main(String[] args) {
       Flyable f = new Cat();
       f.fly();
    }
}
class Animal{

}
interface Flyable{
    void fly();
}
class Cat extends Animal  implements Flyable{
    public void fly(){
        System.out.println("飞猫起飞!");
    }
}
/*
飞猫起飞!
*/

接口在开发中的作用

接口在开发中的作用,类似多态在开发中的作用。

public interface FoodMenu {
    void xiHongShijiDan();
    void yuXiangRouSi();
}
public class Customer {
    //凡是可以用has a形容的,统一用属性的方式存在
    //顾客有一个菜单
    private FoodMenu foodMenu;

    public Customer() {
    }
    public Customer(FoodMenu foodMenu) {
        this.foodMenu = foodMenu;
    }

    public FoodMenu getFoodMenu() {
        return foodMenu;
    }

    public void setFoodMenu(FoodMenu foodMenu) {
        this.foodMenu = foodMenu;
    }
    public void order(){
        //FoodMenu fm = this.getFoodMenu();
        foodMenu.xiHongShijiDan();
        foodMenu.yuXiangRouSi();

    }
}
public class ChinaCooker implements FoodMenu{

    public void xiHongShijiDan() {
        System.out.println("西红柿鸡蛋中国味道");
    }
    public void yuXiangRouSi() {
        System.out.println("鱼香肉丝中国味道");
    }
}
public class AmericaCooker implements FoodMenu{
    public void xiHongShijiDan() {
        System.out.println("西红柿鸡蛋美国味道");
    }
    public void yuXiangRouSi() {
        System.out.println("西红柿鸡蛋美国味道");
    }
}
public class Restaurant {
    public static void main(String[] args) {
        FoodMenu cooker1 = new ChinaCooker();
        Customer c1 = new Customer(cooker1);
        c1.order();
    }

}
/*
西红柿鸡蛋中国味道
鱼香肉丝中国味道
*/

类和类之间的关系

is a、has a、 like a

  • is a:Cat is a Animal 表示继承关系
    凡是满足is a的表示继承关系
  • has a:I has a Pen表示关联关系
    关联关系通常以属性形式存在
  • like a:Cooker like a FoodMenu表示实现关系
    实现关系通常是类实现接口

抽象类和接口的区别

  • 抽象类是半抽象的,接口是完全抽象的。
  • 抽象类有构造方法,接口没有构造方法。
  • 接口之间支持多继承,类之间只有单继承。
  • 一个类可以实现多接口,一个抽象类只能继承一个类。
  • 接口中只能出现常量和抽象方法。
  • 一般接口使用的比较多,接口一般是对行为的抽象。

包机制

  • package是Java包机制。是为了方便程序的管理,不同功能的类分别存放在不同的包下。

  • package是一个关键字,后面加包名。例如:package com.baidu.javase.chapter17。

  • package只允许出现在Java源代码的第一行(注释行不算)。

  • 包名一般采用公司域名倒序的方式。

    • 包名命名规范:公司域名倒序+项目名+模块名+功能名。

package的Java程序编译和运行:

//在chapter17目录下的HelloWorld.java文件
package com.baidu.javase.chapter17;
//此时类名已变为com.baidu.javase.chapter17.HelloWorld
public static void main(String[] args){
    System.out.println("Hello World!");
}

在CMD中编译:

javac -d . HelloWorld.java

javac负责编译
-d带包编译
. 代表编译之后生成的东西放在当前目录下
HelloWorld.java 被编译的java文件名

在CMD中运行:

java com.baidu.javase.chapter17.HelloWorld

使用包机制

  • 如果两个程序在同一package下,包名可以省略。不在同一包名下,必须写全包名。
package com.baidu.javase.chapter17;
public class Test1{
	public static void main(String[] args){
		com.baidu.javase.chapter17.HelloWorld hw = new com.baidu.javase.chapter17.HelloWorld();
		HelloWorld hw2 = new HelloWorld();//同一包名下,可以省略包名
	}
}
package com;
public class Test2{
	public static void main(String[] args){
		//Test1在com.baidu.javase.chapter17包下,Test2在com包下,不能省略包名
		com.baidu.javase.chapter17.HelloWorld hw = new com.baidu.javase.chapter17.HelloWorld();

	}
}

使用import机制

  • import语句只能出现在package语句之下,class声明语句之上。
  • 当两个类不在同一包时,使用import。在同一包内,不需要使用。
  • lang包下的东西都不需要手动导,程序自动导。
package com;
import com.baidu.javase.chapter17.HelloWorld;//将需要的类导入
public class Test3{
	public static void mian(String[] args){
		HelloWorld hw = new HelloWorld();
	}
}
package com.baidu.javase.chapter17;

public class Test4{
	public static void mian(String[] args){
		java.util.Scanner s = new java.util.Scanner(System.in);//必须写明包
	}
}
package com.baidu.javase.chapter17;
import java.util.Scanner;
public class Test5{
	public static void mian(String[] args){
		//此类和Scanner类不在同一个包
		java.util.Scanner s = new java.util.Scanner(System.in);//必须写明包
	}
}

在Test5中也可以写为:

import java.util.*;//*在此只能代表某些类的名字

此种写法运行速度不会比以上写法慢。

访问权限控制符

  • private 私有
  • protected 受保护
  • public 公开

访问控制修饰符的控制范围

  • private 只能在本类中访问。
  • protected 只能在本类,同包,子类中访问。
  • public 在任何位置都能访问。
  • default(默认)只能在本类和同包下访问。
    范围由大到小:public > protected > default > private

访问控制修饰符修饰的对象

  • 属性(4个都能用)
  • 方法(4个都能用)
  • 类(内部类除外)(public和默认可以)
  • 接口(public和默认可以)

Object类

Object类中的常用方法可以查阅Java类库的帮助文档。

  • API是应用程序编程接口。
  • 整个JDK类库就是一个Java SE的API。
  • 每一个API都会配置一个API帮助文档。

目前需要掌握的Object的方法:

protected Object clone()//负责对象克隆
int hashCode()//获取对象哈希值的一个方法
boolean equals(Object obj)//判断两个对象是否相等
String toString()//将对象转换成字符串形式
protected void finalize()//垃圾回收器负责调用的方法

Object类的toString方法

该方法会返回一个“以文本方式表示”此对象的字符串。

  • Object类的toString法可以将一个Java对象转换成字符串表示形式。

    该字符串由类名加标记@和此对象哈希码的无符号十六进制表示组成。

  • Object 类toString源代码如下:getClass().getName() + '@' + Integer.toHexString(hashCode())

  • System.out.println()里面为一个对象的引用时,自动调用toString方法将对象打印出来。

    如果重写了 toString()方法,则调用重写的toString()方法 。

public class test {
    public static void main(String[] args) {
        MyTime t1 = new MyTime(1970,1,1);
        String s1 = t1.toString();
        System.out.println(s1);
    }
}
class MyTime{
    int year;
    int month;
    int day;
    public MyTime(){

    }
    public MyTime(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}
/*
MyTime@49e4cb85
*/

但是我们并不期待这样的结果,我们期待的是具体的日期结果,所以最好重写toString()方法。

  • 建议所有子类都重写toString()方法。
public class test {
    public static void main(String[] args) {
        MyTime t1 = new MyTime(1970,1,1);
        String s1 = t1.toString();
        System.out.println(s1);
    }
}
class MyTime{
    int year;
    int month;
    int day;
    public MyTime(){

    }
    public MyTime(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
   public String toString(){
        return this.year + "年" + this.month + "月" + this.day + "日";
   }
}
/*
1970年1月1日
*/

Object类的equals方法

Sun公司设计equals方法的目的是判断两个对象是否相等的。Object类中equals方法默认源代码是“==”判断,而这只能判断两个引用类型的地址是否一致。在实际开发中往往要重写equals方法来进行比较两个对象。

  • equals方法用来判断两个对象是否相等。
  • “==”用来判断两个基本数据类型是否相等(不能用于两个对象比较),equals用来判断两个引用数据类型是否相等。
  • String类重写了equals方法,可以比较两个字符串,比较两个字符串不能用“==”。必须使用equals判断。
  • String类重写了toString方法,因此不会输出String@十六进制的地址。

Object类中equals方法的源代码:

public boolean equals(Object obj){
	return (this == obj);//源代码中用的“==”,不能用于比较两个引用类型
}
public class test  {
    public static void main(String[] args) {
        MyTime t1 = new MyTime(1970,1,1);
        MyTime t2 = new MyTime(1970,1,1);
        //判断两个Java对象不能使用“==”
        System.out.println(t1 == t2);//false 此处判断的是t1、t2中保存的对象内存地址是否相等
    }
}
class MyTime{
    int year;
    int month;
    int day;
    public MyTime(){

    }
    public MyTime(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}

重写equals方法

public class test {
    public static void main(String[] args) {
        MyTime t1 = new MyTime(1970,1,1);
        MyTime t2 = new MyTime(1970,1,1);

        System.out.println(t1==t2);//false
        System.out.println(t1.equals(t2));//true
    }
}
class MyTime{
    int year;
    int month;
    int day;
    public MyTime(){

    }
    public MyTime(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public boolean equals(Object obj) {//重写equals方法,自定义比较方法,还可以精简,两个if合并,return返回值结合
        if(obj == null){
		return false;
	}
	//想要访问子类中特有的东西
	if(!(obj instanceof MyTime)){//判断obj是不是MyTime类型
		return false;
	}
        if(this == obj){//如果地址都相同就不用比下面了
            return true;
        }
        //向下转型 强转
        MyTime mt = (MyTime) obj;//参数传过来的是Object类型:Object obj = new MyTime(1970,1,1);Object类中没有相应的属性
        //当年月日都相等时,认为两个对象相等
        if(this.year == mt.year&&this.month == mt.month&&this.day == mt.day){//如果此处比较的不是数字是字符串就需要用equals方法
            return true;
        }
        return false;
     }
}

比较两个字符串必须用equals方法,String类重写了equals方法,因此可以比较。

public class test {
    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = new String("Hello");
        String s4 = new String("Hello");
        System.out.println(s1==s2);//true 此处的是s1和s2在常量池中是一个地址,因此为true。
        System.out.println(s1.equals(s2));//true
        System.out.println(s3==s4);//false
        System.out.println(s3.equals(s4));//true
    }
}

String类重写了toString()方法

public class test {
    public static void main(String[] args) {
        String x = new String("Hello");
        System.out.println(x);//引用数据类型当不写toString方法时,自动调用toString方法
        System.out.println(x.toString());//如果没重写toString,结果应为String@十六进制的地址
    }
}
/*
Hello
Hello
*/
public class test {
    public static void main(String[] args) {
        Student s1 = new Student(111,"北京");
        Student s2 = new Student(112,"北京");
        System.out.println(s1 == s2);//false
        System.out.println(s1.equals(s2));//true

//        Student s3 = new Student(111,new String("北京"));
//        Student s4 = new Student(112,new String("北京"));//两个北京的内存地址不同,如果用equals比较那两者通用
//        System.out.println(s3 == s4);//false
//        System.out.println(s3.equals(s4));//false
    }
}
class Student{
    int no;
    String school;
    public Student() {
    }

    public Student(int no, String school) {
        this.no = no;
        this.school = school;
    }

    public String toString() {
        return "学号"+no+"学校名称"+school;
    }
    public boolean equals(Object obj) {
        if(obj == null||!(obj instanceof Student)) return false;
        if(this == obj) return true;
        Student s = (Student) obj;
        return this.no == s.no && this.school.equals(s.school);//比较字符串相等用equals,这里String已经重写好了。
        //return this.no == s.no && this.school == s.school);//不可以
    }
}

equals方法的深层次理解

重写equals方法要彻底

public class test {
    public static void main(String[] args) {
        Address addr1 = new Address("北京","海淀区","111");
        User u1 = new User("张三",addr1);
        // User u1 = new User("张三", new Address("北京","海淀区","111"));
        User u2 = new User("张三", new Address("北京","海淀区","111"));
        System.out.println(u1.equals(u2));//true
    }
}
class User{
    String name;
    Address addr;
    public User() {
    }
    public User(String name, Address addr) {
        this.name = name;
        this.addr = addr;
    }
    //重写equals方法
    //当一个用户的姓名和地址都相同时,表示是同一个用户。这里判断两个User对象是否相等
    public boolean equals(Object obj) {
       if(obj == null||!(obj instanceof User)) return false;
       if(this == obj) return true;
        User u = (User)obj;
       if(this.name.equals(u.name) && this.addr.equals(u.addr)){
            return true;
       }
       return false;
    }
}
class Address{
    String city;
    String street;
    String zipcode;
    public Address() {
    }
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
    //这里不重写equals方法会导致上面的条件里addr的equals比较的是内存地址
    public boolean equals(Object obj) {
        if(obj == null||!(obj instanceof Address)) return false;
        if(this == obj) return true;
        Address a = (Address)obj;
        if(this.city.equals(a.city) && this.street.equals(a.street) && this.zipcode.equals(a.zipcode)){
            return true;
        }
        return false;
    }
}

Object类的finalize方法(了解即可,已过时)

  • 当一个对象即将被垃圾回收器(Garbage Collection)回收时,会自动调用finalize()。
  • JDK9之后已删除此方法。建议不要使用此方法,如果执行此方法出现错误,程序不会报告,仍然继续运行。
  • 在Object类中的源代码:protected void finalize() throws Throwable{}只有一个方法体。
  • finalize是一个垃圾销毁时机,如果希望在对象即将被销毁时执行一段代码,这段代码写在finalize方法中。
public class test {
    public static void main(String[] args) {
     for (int i=0;i<1000000;i++){
         Person p = new Person();
         //将p变成垃圾
         p = null;
     }
    }
}
class Person{
    //重写finalize方法
    //Person类的对象被垃圾回收器回收时,垃圾回收器负责调用p.finalize();
    protected void finalize() throws Throwable {
        System.out.println("即将被销毁");
    }
}
  • 垃圾回收器不是轻易启动的,垃圾太少或者时机未到等条件下可能启动可能不启动。
  • 垃圾回收是在后台运行的,我们无法命令垃圾回收器马上回收资源,但是我们可以告诉他,尽快回收资源(System.gcRuntime.getRuntime().gc())。
system.gc();//建议gc启动,可能听,也可能不听,仅仅是概率大一些

Object类的hashCode方法

  • hashCode()方法返回的是哈希码:实际上就是一个内存地址,经过哈希算法得到一个值。
  • hashCode的源代码:public native int hashCode();
  • hashCode()方法带有native关键字,底层调用了C++程序。
public class test {
    public static void main(String[] args) {
        Object o = new Object();
        int hashCode = o.hashCode();
        System.out.println(hashCode);//2083562754
    }
}

匿名内部类

  • 内部类:在类的内部又定义了一个新的类。

  • 内部类分为:静态内部类、实例内部类、局部内部类。

    • 静态内部类:

      静态内部类不会持有外部的类的引用,创建时可以不用创建外部类。

      静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问。

    • 实例内部类:

      创建实例内部类,外部类的实例必须已经创建。

      实例内部类会持有外部类的引用。

      实例内部不能定义 static 成员,只能定义实例成员。

    • 局部内部类:在方法中定义,只能在当前方法中使用。

      不能包含静态成员。

class Test{
    static class Inner1{//静态内部类
      
    }
    class Inner2{//实例内部类

    }
    public void doSome(){
        int i = 100;//局部变量
        class Inner3{//局部内部类
		
		}
    }
    public void doOther(){
		//doSome()中的局部内部类Inner3,在doOther()中不能用
    }
}
  • 匿名内部类是局部内部类的一种,没有类名。

未使用匿名内部类

public class Test {
    public static void main(String[] args) {
        MyMath mm = new MyMath();
        mm.MySum(new ComputerImp(),100,200);
    }
}
interface Computer{
    //抽象方法
    int sum(int a,int b);
}
class ComputerImp implements Computer{//接口的实现类
    public int sum(int a, int b) {
        return a + b;
    }
}
class MyMath{
    public void MySum(Computer c,int a,int b){
        int retValue = c.sum(a,b);
        System.out.println(a+"+"+b+"="+retValue);
    }
}

使用匿名内部类,可以不写接口的实现类。

public class Test {
    public static void main(String[] args) {
        MyMath mm = new MyMath();
        mm.MySum(new Computer(){
            public int sum(int a,int b){
                return a + b;
            }
        },100,200);
    }
}
interface Computer{
    //抽象方法
    int sum(int a,int b);
}
class MyMath{
    public void MySum(Computer c,int a,int b){
        int retValue = c.sum(a,b);
        System.out.println(a+"+"+b+"="+retValue);
    }
}

二、数组

  • Java中数组是一种引用类型,所以数组对象是在堆内存中,数组的父类是Object类。
  • 数组是一个容器,可以同时容纳多个容器(数组是一组数据的集合)。
  • 数组中可以存基本数据类型,也可以存引用数据类型,但同一数组只能是同一种类型。
  • 数组中存储的是Java对象的话,实际上存储的是对象的引用(内存地址),不能直接存储Java对象。
  • 在Java中,数组一旦创建,长度不可变。
  • 数组作为对象,数组中的元素作为对象的属性,除此之外数组还包括一个成员属性 length,length 表示数组的长度。
  • Java中要求数组的元素类型统一:int类型只能装int类型元素。
  • 数组在内存方面存储时,数组中的元素内存地址是连续的。
  • 数组中首元素的内存地址当作整个数组的内存地址。
  • 数组中每一个元素都是有下标的,从0开始,最后一个元素的下标是length-1。

数组的数据结构的优缺点

优点:

  • 每一个元素的内存地址在空间上是连续的。
  • 每一个元素类型相同,占用空间大小一样。
  • 知道第一个元素的内存地址,知道每一个元素的占用空间,也知道下标,所以可以通过数学表达式计算出某个下标上元素的内存地址,直接通过内存地址定位元素,所以数组的检索效率最高。
  • 数组存储100个和一万个元素在元素查询和检索方面效率是相同的,数组在元素查找时不会一个一个的找,而是通过数学表达式直接算出来的。

缺点:

  • 为了保证数组每个元素内存地址的连续性,所以在数组上随机增删元素的时候,效率较低,因为随即增删元素时会涉及到后面的元素同意向前或者向后位移的操作。
  • 数据不能存储大数据量,因为很难在内存中找到一块特别大的连续的内存空间。
  • 对于数组最后一个元素的增删,是没有效率影响的。

2.1一维数组的声明和初始化

  • 数组元素的类型,可以是Java中的任意类型。

一维数组的声明语法格式有两种:

数据类型[] array;//推荐这种
数据类型 array[];
int[] i;
String str[];
int[] a, b, c;//在一行中也可以声明多个数组

初始化数组有两种方法:

//静态初始化语法格式
int[] array = {100,200,300};
//动态初始化语法格式
int[] a = new int[5];//初始化一个长度为5的int类型数组,默认为该类型的默认值
String[] str = new String[6];//初始化长度为6的String类型数组,每个元素为默认值null 

对一维数组元素的访问

public class test {
    public static void main(String[] args) {
        int[] a = {1,100,10,20,55,689};
        System.out.println("数组中元素个数"+a.length);
        System.out.println("第一个元素"+a[0]);
        System.out.println("最后一个元素"+a[5]);
        System.out.println("最后一个元素"+a[a.length - 1]);
        //修改第一个元素为111;
        a[0] = 111;
        //修改最后一个元素为0;
        a[a.length - 1] = 0;
        System.out.println("第一个元素"+a[0]);
        System.out.println("最后一个元素"+a[a.length - 1]);
    }
}
/*
数组中元素个数6
第一个元素1
最后一个元素689
最后一个元素689
第一个元素111
最后一个元素0
*/

一维数组的遍历:

public class test {
    public static void main(String[] args) {
        int[] a = {1,100,10,20,55,689};
        for (int i=0;i< a.length;i++){//遍历数组
            System.out.println("顺序输出"+a[i]);
        }
        //System.out.println(a[6]);//数组越界 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 6at test2.main(test2.java:7)
        for (int i = a.length - 1;i >= 0;i--){//反向遍历数组
            System.out.println("倒序输出"+a[i]);
        }
    }
}
/*
顺序输出1
顺序输出100
顺序输出10
顺序输出20
顺序输出55
顺序输出689
倒序输出689
倒序输出55
倒序输出20
倒序输出10
倒序输出100
倒序输出1
*/

动态初始化一维数组

public class test {
    public static void main(String[] args) {
        int[] a = new int[6];
        for (int i=0;i< a.length;i++){
            System.out.println("第"+i+"个元素:"+a[i]);
        }
    }
}
/*
第0个元素:0
第1个元素:0
第2个元素:0
第3个元素:0
第4个元素:0
第5个元素:0
*/
  • 当创建数组时,不确定数组中存储哪些数据,用动态初始化方式。

方法的参数是数组时:

public class test {
    public static void main(String[] args) {
        int[] a = {1,2,3,4,5};
        printArray(a);
        String[] str = {"Hello","World","Huawei","harmony"};
        printArray(str);
        String[] str1 = new String[3];
        printArray(str1);
        printArray(new int[]{1,2,3});//直接传一个静态数组进去,中括号里为空,不写长度
    }
    public static void printArray(int[] array){
        for (int i = 0;i< array.length;i++){
            System.out.println(array[i]);
        }
    }
    public static void printArray(String[] array){
        for (int i = 0;i< array.length;i++){
            System.out.println(array[i]);
        }
    }
}
/*
1
2
3
4
5
Hello
World
Huawei
harmony
null
null
null
1
2
3
*/

2.2 关于main方法的String数组

  • JVM调用main方式时,一定会传一个String数组过来,参数默认为0,而非null。

  • 用户可以在控制台输入参数,参数会自动转换成String[] args。

    • 如:在CMD中运行:java 类名 abc def xyz,JVM会自动将abc def xyz用空格分离,放入String args数组中。

这有什么用?

public class Test {
    public static void main(String[] args){
        if(args.length != 2){
            System.out.println("请输入用户名和密码");
            return;
        }
        String username = args[0];
        String password = args[1];
        //此种写法可能导致空指针异常
        //"admin".equals(username)更好
        if(username.equals("admin") && password.equals("123")){
            System.out.println("欢迎回来");
        }else{
            System.out.println("验证失败");
        }
    }
}

2.3 数组中存储的类型为引用数据类型

public class Test{

    public static void main(String[] args){
        Animal a1 = new Animal();
        Animal a2 = new Animal();
        Animal[] animals = {a1,a2};
        for(int i = 0;i < animals.length;i++){
            Animal a = animals[i];
            a.move();
            //animals[1].move();
        }
        Animal[] ans = new Animal[2];
        ans[1] = new Cat();//父类数组可以放子类
        Animal[] anis = {new Cat(),new Bird()};//同时放两个不同的子类也行
        for(int i = 0;i < anis.length;i++){
	    Cat cc = anis[i];//会报错,需要强转
            Animal cc = anis[i];//用父类调
            //如果调用子类特有元素,必须向下转型了
            if(anis[i] instanceof Cat){
                Cat cat = (Cat)anis[i];
                cat.zhua();
            }else if(anis[i] instanceof Bird){
                Bird bird = (Bird)anis[i];
                bird.sing();
            }
        }
    }
}
class Animal{
    public void move(){
        System.out.println("动物在移动");
    }
}
class Cat extends Animal{
    public void move(){
        System.out.println("猫猫在移动");
    }
    public void zhua(){
        System.out.println("猫猫在抓老鼠");
    }
}
class Bird extends Animal{
    public void move(){
        System.out.println("鸟在移动");
    }
    public void sing(){
        System.out.println("鸟儿歌唱");
    }
}
/*
动物在移动
动物在移动
猫猫在抓老鼠
鸟儿歌唱
*/

2.4 数组扩容

  • 在Java中,数组长度一旦确定就不可改变。
  • 数组扩容的方法是:先建一个大容量的数组,然后将小容量数组中的数据一个一个拷贝到大数组当中。
  • 数组扩容效率较低,尽可能的少进行数据拷贝,在创建前预估好数组容量。
System.arraycopy();//内置的系统扩容函数,需要传五个参数

//拷贝源
int [] src = {1,11,22,3,4};
//拷贝目标
int [] dest = new int[20];
//调用JDK System类中的arraycopy方法,来完成数组的拷贝
System.arraycopy(src,1,dest,3,2);
//System.arraycopy(拷贝源,源起点,拷贝目标,目标起始下标,拷贝长度);
//System.arraycopy(src,0,dest,0,src.length);全部拷贝完
//遍历目标数组
for(int i = 0;i < dest.length;i++){
    System.out.println(dest[i]);//0 0 0 11 22 0...0
}
  • 数组中存储的元素是引用类型,也可以拷贝。
//拷贝源
String [] strs = {"Hello","World","study","java","oracle"};
//拷贝目标
String [] newStrs = new String[20];
//调用JDK System类中的arraycopy方法,来完成数组的拷贝
System.arraycopy(strs,0,newStrs,0,strs.length);

//遍历目标数组
for(int i = 0;i < newStrs.length;i++){
    System.out.println(newStrs[i]);//"Hello","World","study","java","oracle" null...null
}

//在拷贝引用对象时,拷贝的是对象的地址,而不是对象
Object [] obj = {new Object(),new Object()};

2.5 二维数组

  • 二维数组是特殊的一维数组,二维数组相当于一维数组中每个元素都为一个一维数组。
  • 三维数组相当于二维数组中每个元素都为一个二维数组,基本用不到三维。

二维数组的静态初始化

int[][] array ={{1,2},{3,4}};
int array[][] ={{1,2},{3,4}};

二维数组的动态初始化

int[][] data = new int[2][3];

//创建不规则二维数组
int[][] data = new int[2][]; 
data[0] = new int[2];
data[1] = new int[4];

2.5.1 二维数组的length属性

int[][] a ={
		{1,2},
		{3,4,5}
	};
System.out.println(a.length);//二维数组中有2个一维数组
System.out.println(a[1].length);//第一行的一维数组有2个元素

2.5.2 二维数组的元素访问

 int[][] a = {
                {34,4,65},
                {100,200,3900,111},
                {0}
        };
	//a[第几个一维数组][第几个一维数组的第几个元素]
        //取出第2个数组中的第一个元素
        System.out.println(a[1][0]);

2.5.3 二维数组的遍历

int[][] a = {
                {34,4,65},
                {100,200,3900,111},
                {0}
        };
	//遍历二维数组
        for(int i = 0;i < a.length;i++){//外层循环三次,负责纵向
            for (int j = 0;j < a[i].length;j++){//内层循环每层一维的长度,负责横向
                System.out.println(a[i][j]);
            }
        }

2.5.4 方法的参数是一个二维数组时

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

        int[][] a = {
                {34,4,65},
                {100,200,3900,111},
                {0}
        };
        printArray(a);
    }
    public static void printArray(int[][] array){//遍历二维数组
        for(int i = 0;i < array.length;i++){
            for (int j = 0;j < array[i].length;j++){
                System.out.println(array[i][j]);
            }
        }
    }
}

2.6 一维数组的内存结构

image.png

2.7 数组模拟栈数据结构

要求:

  1. 这个栈可以存储Java中任何引用类型的数据。
  2. 在栈中提供push方法模拟压栈。
  3. 在栈中提供pop方法模拟弹栈。
  4. 编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。

MyStack.java

public class MyStack {
    private Object[] elements;
    private int index;
  
    public MyStack() {
        this.elements = new Object[10];//默认栈的初始化容量为10
        this.index = -1;//index为-1表示栈帧指向顶部元素,为0表示指向顶部元素的上方
    }
    //压栈方法
    public void push(Object obj){
        if(this.index >= this.elements.length - 1){
            System.out.println("压栈失败,栈已满!");
            return;
        }
        //走到此处说明栈没满
        //向栈中加一个元素,栈帧向上移动一个位置
        this.index++;
        this.elements[index] = obj;
        //在sout执行时,如果输出引用的话,自动调用引用的toString()方法
        System.out.println("压栈 " + obj + " 成功!栈帧指向:" + this.index);
    }
    //弹栈方法
    public void pop(){
        if(this.index < 0){
            System.out.println("弹栈失败,栈已空!");
            return;
        }
        //栈没空
        System.out.print("弹栈 " + this.elements[index] + "成功!");
        index--;
        System.out.println("栈帧指向:" + this.index);
    }
    public int getIndex() {
        return index;
    }
    public void setIndex(int index) {
        this.index = index;
    }
    public Object[] getElements() {
        return elements;
    }
    public void setElements(Object[] elements) {
        this.elements = elements;
    }
}

StackSimulation.java

public class StackSimulation {
    public static void main(String[] args) {
        MyStack stack = new MyStack();
        stack.push(new Object());
        stack.push(new Object());
        stack.push(new Object());
        stack.push(new Object());
        stack.push(new Object());
        stack.push(new Object());
        stack.push(new Object());
        stack.pop();
        stack.pop();
        stack.pop();
        stack.pop();
    }
}

2.8 酒店管理系统模拟

要求:

  1. 该系统的用户是酒吧前台。

  2. 酒店中所有的房间使用一个二维数组来模拟。

  3. 酒店中每一个房间应该是一个Java对象。

  4. 每一个房间Room应该有:房间编号、房间类型属性、房间是否空闲。

  5. 系统对外提供的功能:

    1. 可以预订房间:用户输入房间号订房。
    2. 可以退房:用户输入房间编号退房。
    3. 可以查看所有房间的状态:用户输入某个指令应该可以查看所有房间状态。

Hotel.java

public class Hotel {
    private Room[][] rooms;
    public Hotel(){
        rooms = new Room[3][10];//三层每层十个房
        for(int i = 0;i < rooms.length;i++){//i+1是楼层
            for (int j = 0;j < rooms[i].length;j++){
                if(i == 0){
                    rooms[i][j] = new Room((i+1)*100+j+1,"单人间",true);
                }else if(i == 1){
                    rooms[i][j] = new Room((i+1)*100+j+1,"标准间间",true);
                }else if(i == 2){
                    rooms[i][j] = new Room((i+1)*100+j+1,"总统套房",true);
                }

            }
        }

    }
    public void print(){
        for(int i = 0;i < rooms.length;i++) {//i+1是楼层
            for (int j = 0; j < rooms[i].length; j++) {//负责输出一层
                Room room = rooms[i][j];
                System.out.print(room);
            }
            System.out.println();
        }
    }
    public void order(int roomNo){//订房
        Room room = rooms[roomNo / 100 - 1][roomNo % 100 - 1];
        room.setStatus(false);
        System.out.println(roomNo+"已订房!");
    }
    public void exit(int roomNo){//退房
        Room room = rooms[roomNo / 100 - 1][roomNo % 100 - 1];
        room.setStatus(true);
        System.out.println(roomNo+"已退房!");
    }
}

Room.java

public class Room {
    private int no;

    private String type;

    private boolean status;

    public Room() {

    }

    public Room(int no, String type, boolean status) {
        this.no = no;
        this.type = type;
        this.status = status;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getType() {
        return type;
    }

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

    public boolean getStatus() {
        return status;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null || !(obj instanceof Room)) return false;
        if(this == obj) return true;
        Room room = (Room)obj;
        return this.no == room.getNo();
    }

    @Override
    public String toString() {
        return "[" + no + "," + type + "," + (status ? "空闲" : "占用") + "]";
    }
}

HotelSimulation.java

import java.util.Scanner;

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

        Hotel hotel = new Hotel();

        System.out.println("欢迎使用酒店管理系统");
        while (true){
            System.out.println("功能编号对应的功能:");
            System.out.println("[1] 查看房间列表");
            System.out.println("[2] 订房");
            System.out.println("[3] 退房");
            System.out.println("[0] 退出系统");
            Scanner s = new Scanner(System.in);
            System.out.println("请输入功能编号:");
            int i = s.nextInt();
            if(i == 1){
                hotel.print();
            }else if (i == 2){
                System.out.println("请输入订房编号:");
                int roomNo = s.nextInt();
                hotel.order(roomNo);
            }else if (i == 3){
                System.out.println("请输入退房编号:");
                int roomNo = s.nextInt();
                hotel.exit(roomNo);
            }else if (i == 0){
                System.out.println("再见!");
                return;
            }
        }

    }
}

2.9 Arrays工具类

java.util.Arrays

  • 工具类中方法大部分都是静态的。

Arrays.sort()方法的使用binarySearch

int[] arr ={112,3,4,56,67,1};
//给数组排序
Arrays.sort(arr)//从小到大排序
for(int i = 0;i < arr.length;i++){
    System.out.println(arr[i]);//1 3 4 56 67 112
}

Arrays.fill()方法的使用

int array[] = new int[6];

//Arrays.fill(int[] a, int val)
//将指定的 int 值分配给指定 int 型数组的每个元素
Arrays.fill(array, 100);
for (int i=0, n=array.length; i < n; i++) {
    System.out.println(array[i]);
}
System.out.println();
//Arrays.fill(int[] a, int fromIndex, int toIndex, int val)
//将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。
//填充的范围从索引 fromIndex(包括)一直到索引 toIndex(不包括)。(如果 fromIndex == toIndex,则填充范围为空。)
Arrays.fill(array, 3, 6, 50);
for (int i=0, n=array.length; i< n; i++) {
    System.out.println(array[i]);
}
/*
100
100
100
100
100
100

100
100
100
50
50
50
*/

2.10 冒泡排序算法

public class BubbleSort {
    public static void main(String[] args) {
        /*冒泡排序法:
            3,2,7,6,8
            第一次循环(最大的交换到右边):
            3和2比较结果:2,3,7,6,8
            3和7比较结果:2,3,7,6,8
            7和6比较结果:2,3,6,7,8
            7和8比较结果:2,3,6,7,8
            第二次循环:(第二大的交换到右边)
            2和3比较结果:2,3,6,7
            3和6比较结果:2,3,6,7
            6和7比较结果:2,3,6,7
            第三次循环:(第三大的交换到右边)
            2和3比较结果:2,3,6
            3和6比较结果:2,3,6
            第四次循环:(第四大的交换到右边)
            2和3比较结果:2,3
        */
        int[] arr = {45,3,6,11,678,224,9,0};

        for(int i = 0;i < arr.length - 1;i++){
            System.out.println("第" + (i+1) + "次循环");
            for(int j = 0;j < arr.length - 1 - i;j++){
                if(arr[j] > arr[j+1]){
                    int temp;
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    System.out.println(arr[j] + "和" + arr[j+1] + "交换位置:" + f(arr));
                }else {
                    System.out.println(arr[j] + "和" + arr[j+1] + "输出结果:" + f(arr));
                }
            }
        }
    }
    public static String f(int[] array){
        String str = "";
        for(int i = 0;i < array.length;i++){
            str = str + array[i] + " ";
        }
        return str;
    }
}

2.11 选择排序算法

public class SelectionSort {
    public static void main(String[] args) {
        //选择排序
        /*
            选择排序比冒泡排序效率高
            选择排序每次交换都是有意义的
            循环一次,然后找出参与比较的数据中的最小值,将这个值和最前数据作比较交换位置

            参与比较的数据:3 1 6 2 5
            第一次循环:
            1 3 6 2 5
            参与比较的数据:3 6 2 5
            第二次循环:
            2 6 3 5
            参与比较的数据:6 3 5
            第三次循环:
            3 6 5
            参与比较的数据:6 5
            第四次循环:
            5 6
        */
        int[] arr = {3,1,6,2,5};
        for (int i =0;i<arr.length - 1;i++){
            //i是参与比较数据的最左下标
            //假设起点i下标位置上的数据是最小的
            //3 2 6 1 5
            //假设3最小,拿3和2比,发现2小,拿着2再和6比较,发现2小,再拿着2和1比较,发现1小,拿着1和5比,还是1小,所以最小为1
            int min = i;
            for (int j = i+1;j < arr.length;j++){
                //找出参与运算数据中最小值,然后替换最前数据
                if(arr[j] < arr[min]){
                    min = j;//最小值元素下标为j
                }
            }
            //当i和min相等时,表示猜测正确
            //猜测错误时,需要拿着更小的和最左边元素交换
            if(min != i){
                //存在更小的数据
                //arr[min]最小数据
                //arr[i]最前数据
                int temp;
                temp = arr[min];
                arr[min] = arr[i];
                arr[i] = temp;
            }
            }
            //遍历数组
            for(int i = 0;i < arr.length;i++){
                System.out.println(arr[i] + " ");
            }

    }
}

2.12 二分法查找

public class ArraySearch {
    public static void main(String[] args) {
        /*
        二分法查找,必须按从小到大顺序排序的数组
        10(下标0) 11 12 13 14 15 16 17 18 19 20(下标10)
        通过二分法查找找出18这个元素的下标
        (0+10)/2计算出中间元素下标:5
        拿着中间元素和目标查找元素对比
        15<18  18在中间元素的右侧
        把开始下标元素改为5+1,再重新计算中间元素下标为8,对应中间元素18
        18 = 18表示找到了
         */

        int[] arr = {100,200,230,235,600,1000,2000,9999};
        //找出数组中200所在的下标
        int index = binarySearch0(arr,20);
        System.out.println(index == -1 ? "该元素不存在" : "该元素下标为:"+index);
    }
    public static int binarySearch0(int[] arr,int dest){
        int begin = 0;
        int end = arr.length - 1;
        while(begin <= end){//只要开始在结束前,就继续循环
            int mid = (begin + end)/2;
            if(arr[mid] == dest){
                return mid;
            }else if(arr[mid] < dest){
                //在中间的右边
                begin = mid + 1;
            }else if(arr[mid] > dest){
                //在中间的左边
                end = mid - 1;
            }
        }
        return -1;
    }
}

三、常用类

3.1 String字符串的存储原理

java.lang.String

  1. String表示字符串类型,属于引用类型

  2. Java中使用双引号括起来的都是String对象

  3. Java中规定,双引号括起来的字符串是不可变的,直接存储在方法区中的字符串常量池中

  4. String为什么是不可变的?

    我看过源代码,String了类有一个byte[]数组,这个数组采用final修饰哈哈哈哈,因为数组一旦创建长度就不可变,并且被final修饰的引用一旦指向某个对象之后,不可指向其他对象,所以不可变

  5. StringBuffer和StringBuilder为什么是可变的?

    我看过源代码,他们内部实际上是一个byte[]数组,这个数组没有被final修饰,初始化容量为16,当存满后会进行扩容,底层调用了数组拷贝的方法

3.2 String类的构造方法

public class StringTest {
    public static void main(String[] args) {
        //创建字符串最常用的一种方法
        String str1 = "Hello World";
        /*String的常用的构造方法
        String s = new String("");
        String s = "";
        String s = new String(char数组);
        String s = new String(char数组,起始下标,长度);
        String s = new String(byte数组);
        String s = new String(byte数组,起始下标,长度);
         */

        byte[] bytes = {97,98,99};
        String str2 = new String(bytes);
        System.out.println(str2);//abc,String类已经重写了toString方法,输出字符串对象,输出的是字符串本身

        String str3 = new String(bytes,1,2);//(字节数组,数组元素下标起始位置,长度)
        System.out.println(str3);//bc

        char[] chars = {'a','b','c','d','e'};
        //将char数组的一部分转换成字符串
        String str4 = new String(chars);
        System.out.println(str4);//abcde
        String str5 = new String(chars,2,3);
        System.out.println(str5);//cde
    }
}

3.3 String类的常用方法

 public class StringTest {
    public static void main(String[] args) {
        /*
        String类的常用方法
        char charAt(int index)
        int compareTo(String antherString)
        boolean contains(CharSequence s)
        boolean endsWith(String suffix)
        boolean equalsIgnoreCase(String anotherString)
        byte getBytes()
        int indexOf(String str)
        boolean isEmpty()
        int length()
        int lastIndexOf(String str)
        String replace(CharSequence target, CharSequence replacement)
        String[] split(String regex)
        boolean startsWith(String prefix)
        String substring(int beginIndex)
        char[] toCharArray()
        String toLowerCase()
        String trim()
        static String valueOf()
         */

        char c = "中国人".charAt(1);
        System.out.println(c);//国

        System.out.println("abc".compareTo("abc"));//返回0,按字典顺序比较两个字符串
        System.out.println("abcd".compareTo("abce"));//返回-1,前小后大负数
        System.out.println("abce".compareTo("abcd"));//返回1,前大后小正数

        System.out.println("HelloWorld.java".contains(".java"));//true,判断前面是否包含后面的字符串

        System.out.println("text.txt".endsWith(".java"));//false,判断当前字符串是否以某个字符串结尾

        System.out.println("Hello".equalsIgnoreCase("hello"));//true,判断两个字符串是否相等,忽略大小写

        byte[] bytes = "abcdef".getBytes();//将字符串对象转换成字节数组
        for (int i = 0;i < bytes.length;i++){
            System.out.println(bytes[i]);//97 98 99 100 101 102
        }

        System.out.println("oraclejavac++.netphppython".indexOf("c+"));//10 判断某个字符串在当前字符串中第一次出现处的索引

        System.out.println("".isEmpty());//true,判断字符串是否为空

        System.out.println("abc".length());//3,判断字符串长度,字符串长度的length()是方法,判断数组的length是属性

        System.out.println("oraclejavac++.netphppython".lastIndexOf("oracle"));//10 判断某个字符串在当前字符串中最后一次出现处的索引

        System.out.println("www.baidu.com".replace("baidu","jd"));//www.jd.com,将字符串中与文字目标匹配的每个子字符串替换为指定的文字

        System.out.println();//以”-“为分隔符拆分字符串
        String[] s = "1980-10-1".split("-");
        for (int i = 0;i < s.length;i++){
            System.out.println(s[i]);//1980 10 1
        }

        System.out.println("text.txt".endsWith("text"));//true,判断当前字符串是否以某个字符串开头

        System.out.println("www.baidu.com".substring(4));//baidu.com,截取一段字符串
        System.out.println("www.baidu.com".substring(4,9));//baidu.com,截取一段字符串,左闭右开

        char[] chars = "我是中国人".toCharArray();
        for (int i = 0;i < chars.length;i++){
            System.out.println(chars[i]);//我 是 中 国 人,将字符串转换成char数组
        }

        System.out.println("ABCDEFg".toLowerCase());//abcdefg,转换为小写
        System.out.println("abcdefg".toUpperCase());//ABCDEFG,转换为大写

        System.out.println("  Hello   World  ".trim());//去除字符串前后空白,不会去掉中间空白

        String s1 = String.valueOf(true);//将非字符串转换为字符串
        System.out.println(s1);//true,这是个字符串
        String s2 = String.valueOf(new Customer());
        System.out.println(s2);//内存地址,自动调用了toString()方法
    }
}
class Customer{

}

3.4 StringBuffer字符串拼接

import java.lang.StringBuffer;
public class StringBufferTest {
    public static void main(String[] args) {
        //在实际开发中,需要进行字符串的频繁拼接,这样会占用大量方法区内存,造成内存空间的浪费
        //如果进行大量字符串拼接使用JDK中的
        // java.lang.StringBuffer
        // java.lang.StringBuilder

        /*
        优化StringBuffer的性能:在创建StringBuffer时,尽可能的给定一个初始化容量,减少底层数组的扩容次数
         */
      
        //创建一个初始化容量为16的byte[]数组(字符串缓冲区对象)
        StringBuffer stringBuffer = new StringBuffer();
        //拼接字符串统一用append()方法,追加的意思,如果byte数组满了,会自动扩容
        stringBuffer.append("a");
        stringBuffer.append("b");
        System.out.println(stringBuffer);
    }
}
//StringBuffer和StringBuilder的区别
//StringBuffer是线程安全的,有synchrinized修饰,在多线程环境下运行是安全的。StringBuilder是非线程安全的

3.5 基本数据类型对应的8个包装类

为什么要使用包装类型?

public class IntegerTest00 {
    public static void main(String[] args) {
        //调用doSome()方法时需要传一个数字进去,但是数字属于基本类型,Object属于引用数据类型
        //此时可以对int进行一个包装将数字包装为对象
        MyInt myInt = new MyInt(100);
        doSome(myInt);
    }
    public static void doSome(Object obj){
        System.out.println(obj.toString());
    }
}

MyInt手写包装类

public class MyInt {
    int value;

    public MyInt() {
    }

    public MyInt(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

八个包装类

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

        /*
        8种基本数据类型对应的包装类型名
        byte    java.lang.Byte(父类Number)
        short   java.lang.Short(父类Number)
        int     java.lang.Integer(父类Number)
        long    java.lang.Long(父类Number)
        float   java.lang.Float(父类Number)
        double  java.lang.Double(父类Number)
        boolean java.lang.Boolean(父类Object)
        char    java.lang.Character(父类Object)
        重点学习Integer,其他基本一致
        Number是一个抽象类,无法实例化对象
         */

        Integer i =new Integer(123);
      
        //基本数据类型转换为引用数据类型叫装箱
        float f = i.floatValue();
        System.out.println(f);//123.0

        //引用数据类型转换为基本数据类型叫拆箱
        int retValue = i.intValue();
        System.out.println(retValue);//123

    }
}

Integer的构造方法有:两个

integer(int i);

Integer(String s);

public class IntegerTest02 {
    public static void main(String[] args) {
        //将数字100转换为Integer包装类型
        Integer x = new Integer(100);
        System.out.println(x);
        //将String类型转换为Integer包装类型
        Integer y = new Integer("123");
        System.out.println(y);
        //将数字1.23转换为Double包装类型
        Double z = new Double(1.23);
        System.out.println(z);
        //将String类型转换为Double包装类型
        Double e = new Double("3.14");
        System.out.println(e);

        //通过常量值获取最大值和最小值
        System.out.println("int的最大值" + Integer.MAX_VALUE);
        System.out.println("int的最小值" + Integer.MIN_VALUE);
        System.out.println("byte的最大值" + Byte.MAX_VALUE);
        System.out.println("byte的最小值" + Byte.MIN_VALUE);
    }
}

自动装箱和自动拆箱

public class IntegerTest03 {
    public static void main(String[] args) {
        //在1.5之后,支持自动装箱拆箱了,有了自动拆箱后,Number中的方法就用不到了
        //自动装箱:将int类型转换为Integer
        Integer x = 100;
        //自动拆箱:将Integer类型转换为int
        int y = x;

        Integer z = 900;//z是个对象
        System.out.println(z + 1);//在这里z会自动拆箱变为基本数据类型

        Integer a = 1000;//Integer a = new Integer(1000);a是个引用,保存内存地址指向对象
        Integer b = 1000;//Integer b = new Integer(1000);b是个引用,保存内存地址指向对象
        System.out.println(a == b);//false,==比较多是内存地址,双等号不会自动拆箱

        Integer c = 127;
        Integer d = 127;
        System.out.println(c == d);//true
        /*
        Integer类加载的时候会初始化整数型常量池:256个对象
        Java为了提高效率,将[-128~127]之间的包对象提前创建好,放到一个方法区的整数型常量池中,其实池就是缓存cache
        ,只要是使用这个区间的数据就不需要再new了,直接从常量池中取出,所以c和d变量双等结果是true
         */

    }
}

Integer常用方法

public class IntegerTest04 {
    public static void main(String[] args) {
        /*
        空指针异常:NullPointerException
        类型转换异常:ClassCastException
        数组下标越界异常:ArrayIndexOutOfBoundsException
        数字格式化异常:NumberFormatException
         */


        //Integer常用方法

        //自动装箱
        Integer x = 1000;
        //手动拆箱
        int y = x.intValue();
        System.out.println(y);//1000

        Integer a = new Integer("中文");//不是一个数字可以包装成Integer对象吗?不能,会运行异常NumberFormatException
        int b = a.intValue();
        System.out.println(b);

        //static int   parseInt(String s)
        int retValue = Integer.parseInt("123");//String转换成int
        System.out.println(retValue + 100);//网页文本框中输入的其实是字符串,但是数据库要求保存数字,此时用parseInt()方法

        /*
        ---------------------------------------------------------------------------------
        以下作为了解
        static String toBinaryString(int i) 十进制转二进制
        static String toHexString(int i) 十进制转十六进制
        static String toOctalString(int i) 十进制转八进制
        static Integer valueOf(int i)
        static Integer valueOf(String s)
         */

    }
}

String int Integer三种类型互相转换

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

        //String转int
        int i1 = Integer.parseInt("100");//i1是数字100
        System.out.println(i1 + 1);//101

        //int转String

        String s2 = i1 +"";//"100"字符串
        System.out.println(s2 + 1);//"1001"
        //int转Integer
        Integer x = 1000;

        //Integer转int
        int y = x;


        //String转Integer
        Integer k = Integer.valueOf("123");
        //Integer转String
        String e = String.valueOf(k);
    }
}

3.6 Java语言中对日期的处理

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateTest00 {
    public static void main(String[] args) throws Exception{
        /*
        简单总结System的方法
        System.out out是Ststem类的静态变量
        System.out.println() println()不是System类的,是PrintStream类的方法
        System.gc() 建议启动垃圾回收器
        System.currentTimeMillis() 获取自1970 1 1到系统当前时间的总毫秒数
        System.exit(0)  推出JVM
         */



        //获取系统当前时间
        Date nowTime = new Date();//精确到毫秒的系统当前时间
        System.out.println(nowTime);

        //SimpleDateFormat是java.text包下的,专门负责日期格式化
        /*
        yyyy 年
        MM 月
        dd 日
        HH 时
        mm 分
        ss 秒
        SSS 毫秒
        在日期格式中,除了以上符号不能修改外,其他符号随意组合
         */
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String nowTImestr = sdf.format(nowTime);
        System.out.println(nowTImestr);


        //有一个字符串类型的日期,怎么转换为Date类型
        String time = "2008-08-08 08:08:08 888";//格式不能随便写,必须前后对应
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        Date dateTime = sdf2.parse(time);
        System.out.println(dateTime);

        //获取自1970年1月1日 零时零分零秒到当前时间的总毫秒数
        long nowTimeMillis = System.currentTimeMillis();
        System.out.println(nowTimeMillis);

        //需求:统计一个方法执行所需耗时
        long nowTimeMillis0 = System.currentTimeMillis();
        print();
        long nowTimeMillis1 = System.currentTimeMillis();
        System.out.println(nowTimeMillis1 - nowTimeMillis0);

        //1970年1月1日 0时0分0秒 1毫秒
        Date time0 = new Date(1);//参数是一个毫秒

        //获取昨天此时的时间
        Date time1 = new Date(System.currentTimeMillis() - 1000*60*60*24);
    }

    public static void print(){
        for(int i = 0;i<100;i++){
            System.out.println("i= " + i);
        }

    }
}

3.7 数字格式化

import java.math.BigDecimal;
import java.text.DecimalFormat;

public class Test1 {
    public static void main(String[] args) {
        //DecimalFormat专门负责格式化数字
        /*
        # 代表任意数字
        , 代表千分位
        . 代表小数点
        0 代表不够时补0DecimalFormat
         */
        DecimalFormat df = new DecimalFormat("###,##.##");
        String s = df.format(1234.56);
        System.out.println(s);//1,234.56

        //BigDecimal 属于大数据,精度极高,不属于基本数据类型,属于对象引用数据类型,专门用在财务软件上
        BigDecimal v1 = new BigDecimal(100);//精度极高的100
        BigDecimal v2 = new BigDecimal(200);//精度极高的200
        //相加不能用加号
        BigDecimal v3 = v1.add(v2);
        System.out.println(v3);
    }
}

3.8 随机数

import java.util.Random;

public class test1 {
    public static void main(String[] args) {
        Random random = new Random();
        int num = random.nextInt();//随机生成一个int类型取值范围的随机数
        System.out.println(num);

        random.nextInt(101);//产生[0-100]的随机数,不包括101

        //生成五个不重复的随机数放到数组中
         int[] arr = new int[5];
         for(int i = 0;i < arr.length ;i++){
             arr[i] = -1;
         }
         int index = 0;
         while(index < arr.length){
             int number = random.nextInt(6);
             if(!contains(arr,number)){
                arr[index++] = number;
                //index++;
             }
         }
        for(int i = 0;i < arr.length ;i++){
            System.out.println(arr[i]);
        }

    }
    public static boolean contains(int[] arr,int key) {//专门表示数组中是否包含某个元素
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == key) {
                return true;
            }
        }
        return false;
    }
}

3.9 枚举

public class test01 {
    public static void main(String[] args) {
        //结果超过两种情况并且是可以一个一个列出的,建议使用枚举
        Result r =divide(10,0);
        System.out.println(r == Result.SUCCESS ? "计算成功" : "计算失败");
    }
    public static Result divide(int a,int b){
        try {
            int c = a / b;
            return Result.SUCCESS;
        }catch (Exception e){
            return Result.FAIL;
        }
    }
}
//枚举是一种引用类型,每一个值都可以看做是常量
enum Result{
    //SUCCESS是枚举Result类型中的一个值
    //FAIL是枚举Result类型中的一个值
    //枚举中的值可以看作是"常量"
    SUCCESS,FAIL
}

四、异常

4.1 异常的作用

在程序中,错误可能产生于程序员没有预料到的各种情况,或者超出程序员可控范围的环境。为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。

当程序执行时出现问题,JVM将异常信息打印到控制台,程序员看到异常后进行修改。异常机制可以使程序更健壮。

4.2 异常的继承结构

  • Java中异常以类和对象的形式存在。
  • 异常都是由Throwable继承而来的,下一层分为两个分支:Error和Exception。
  • 异常对象都是派生于Throwable类的一个类实例。
  • 派生于Error类或RuntimeException类的所有异常称为非检查型(unchecked)异常;所有其他异常称为检查型(checked)异常。

Object类下有一个分支:

  1. Throwable类

    1. Error类:不可处理的,会直接退出JVM。Error类描述了Java运行时系统的内部错误和资源耗尽错误。

    2. Exception类:可处理的。

      1. RuntimeExcepiton类:运行时异常(未受检异常或非受控异常),不要求编写阶段对异常提前处理。发生概率低

        由编程错误导致的异常属于RuntimeExcepiton。

      2. Exception的直接子类:编译时异常(受检异常或受控异常),要求编写阶段对异常进行提前处理。发生概率高

image.png

编译时异常和运行时异常都是发生在运行阶段,编译阶段是不会发生异常的。因为只有运行阶段才会new对象,异常的发生就是因为new了异常对象。

常见的编译时异常和运行时异常:

IOException 输入输出流异常
FileNotFoundException 文件找不到的异常
ClassNotFoundException 类找不到异常
DataFormatException 数据格式化异常
NoSuchFieldException 没有匹配的属性异常
NoSuchMethodException 没有匹配的方法异常
SQLException 数据库操作异常
TimeoutException 执行超时异常

运行时异常

ArrayIndexOutofBoundsException 数组越界异常
ClassCastException 类型转换异常
NullPointerException 空指针异常
IllegalAccessException 非法的参数异常
InputMismatchException 输入不匹配异常

4.3 处理异常的两种方法

异常处理有两种方法:将异常上抛或使用try-catch语句捕获

  1. 将异常上抛:在方法声明位置使用throws关键字抛给上一级,Java中异常如果一直上抛,最后会抛给main方法,继续上抛给JVM中止程序运行
  2. 使用try-catch语句捕获:try语句块中某一行出现异常,该行后面的代码不会执行

程序示例:

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

       System.out.println(100/0);
        /*此处发生了ArithmeticException异常,底层对象new了一个ArithmeticException对象,
         然后抛出了,由于是main方法调用了100/0,所以这个异常抛给了main方法,main没有处理,
         自动抛给jvm,jvm终止程序执行
		 
	 ArithmeticException属于运行时异常,在编写阶段不需要对这种异常预先处理
	*/

        //以下未执行
        System.out.println("Hello World");
    }
}
public class HelloWorld {
    public static void main(String[] args) {
        //调用doSome时必须对这种异常进行预先处理,否则报错
        doSome();//报错

    }
    public static void doSome() throws ClassNotFoundException{
        //doSome在执行过程中有可能会出现ClassNotFoundException异常,
        //直接父类是Exception所以ClassNotFoundException属于编译时异常
        System.out.println("doSome");
    }
}
  • 如果一个方法有可能抛出多个检查型异常类型,那么需要在方法首部列出所有的异常,异常类之间使用逗号隔开:

    public Image loadImage(String s)throws FileNotFoundException,EOFException{}

  • 不需要声明Error类继承的异常,我们对此无法控制,也不应声明从RuntimeException继承来的非检查型异常,运行时异常完全在我们的控制中,应该修正而不是声明。

  • 一个方法必须声明所有可能抛出的检查型异常

  • 如果希望调用者来处理就使用throws上报,其余都使用捕捉

  • 如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用(子类方法可以抛出更特定的异常,或者根本不抛出异常)。如果超类没有抛出任何检查型异常,子类也不能抛出任何检查型异常。

    程序示例:

    class Animal{
        public void doSome(){}
        public void doOther() throws Exception{}
    }
    class Cat extends Animal{
        //报错
    //    public void doSome()throws Exception{}
    
        public void doOther(){}
    
        //如果超类没有抛出任何检查型异常,子类也不能抛出任何检查型异常。
        public void doSome()throws Error,RuntimeException{}
    }
    

4.4 异常处理的原理

处理异常的两种方式程序示例:

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

        //第一种处理方式在方法声明的位置上继续使用上抛,抛给调用者
	//public static void main(String[] args) throws ClassNotFoundException {
            //调用doSome时必须对这种异常进行预先处理,否则报错
            //编译时异常必须预先处理
            //doSome();
    	//}

        //第二种方式:try catch进行捕捉,捕捉等于把异常拦截了,把异常真正解决了,调用者是不知道的
        try{
            doSome();
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
    public static void doSome() throws ClassNotFoundException{

        System.out.println("doSome");
    }
}
import java.io.FileInputStream;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }
    private  static void m1(){
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }
    private  static void m2(){
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }
    private  static void m3(){
        //调用SUN jdk中某个类的构造方法
        //创建一个输入流对象,该流指向一个文件

        //有报错,因为构造方法有一个编译时异常
        //FileNotFindException父类是IOException,IOException父类是Exception,由此可知FileNotFindException是编译时异常
        new FileInputStream("C:\\Users\\41882\\Desktop\\harmony_develop");//报错标红
    }
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class HelloWorld {
    public static void main(String[] args) {
        //一般不建议在main方法上使用throws,因为一定会抛给jvm,jvm只会终止。
        //异常处理机制是为了增加程序的健壮性,异常发生了也不会影响程序执行。
        //一般建议使用try catch 进行捕捉 main就不要向上抛了
        System.out.println("main begin");
        try {//try尝试
            m1();
            //m1里出异常,下面就不会执行了
        }catch (FileNotFoundException e){
            //这个分支中可以使用e引用,e引用保存的内存地址就是new出来的异常对象的内存地址
            //catch是捕捉异常后走的分支
            //m1里出异常,执行这里
            System.out.println("文件不存在!");
        }

        System.out.println("main over");
    }
    private  static void m1() throws FileNotFoundException {
        System.out.println("m1 begin");
        m2();
        //m2里出异常,下面就不会执行了
        System.out.println("m1 over");
    }
    private  static void m2() throws FileNotFoundException {
        System.out.println("m2 begin");
        m3();
        //m3里出异常,下面就不会执行了
        System.out.println("m2 over");
    }
    private  static void m3() throws FileNotFoundException {
        //调用SUN jdk中某个类的构造方法
        //创建一个输入流对象,该流指向一个文件

        //有报错,因为构造方法有一个编译时异常
        //FileNotFindException父类是IOException,IOException父类是Exception,由此可知FileNotFindException是编译时异常
        //throws 父类也行,因为父类包括子类,可以多个异常逗号隔开
        new FileInputStream("C:\\Users\\41882\\Desktop\\harmony_develop");
        //如果上面那行代码出异常,下面代码就不会执行了
    }
}
/*
main begin
m1 begin
m2 begin
文件不存在!
main over
*/

4.5 try-catch深入

  • catch后面的类型可以是具体的异常类型,也可以是该类型的父类型(体现了多态)。
  • catch可以写多个,建议写精确点,这样有利于程序的调试,当catch写多个时,必须遵循从上到下、从小到大。
  • JDK7及以上,允许catch( FileNotFoundException | NullPointerException e )的形式捕获多个异常,但是只有当捕获的异常类型彼此之间不存在子类关系时才行。

程序示例:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class HelloWorld {
    public static void main(String[] args) {
	//catch写多个时,必须遵循从上到下、从小到大
        try {
            new FileInputStream("C:\\Users\\41882\\Desktop\\harmony_develop");
        }catch (FileNotFoundException e){
            System.out.println("文件不存在!");
        }catch (IOException e){
            System.out.println("文件不存在!");
        }catch (Exception e){
            System.out.println("文件不存在!");
        }

	//单个catch可以捕获多个异常
        try {
            new FileInputStream("C:\\Users\\41882\\Desktop\\harmony_develop");
        }catch (FileNotFoundException | NullPointerException e){
            System.out.println("文件不存在!");
        }
    }
}
}

4.6 finally子句

  • 在finally子句中的代码是最后执行的,并且一定会执行,即使try中出现异常。finally子句必须和try语句一起出现,不能单独使用
  • 通常在finally中完成资源的释放或者关闭,因为finally一定执行,即使前面出现异常,finally也会正常执行。
  • 实际开发中try-with-resources语句可能比finally子句更常用。
  • try和finally没有catch也可以执行,try不能单独使用。

try中即使用return; finally也依旧会执行

程序示例:

public class HelloWorld {
    public static void main(String[] args) {
	//先执行try,再执行finally,最后return(return语句只要执行就必然程序结束)
        try{
            System.out.println("try");
            return;
        }finally {
            System.out.println("finally");
        }
	//永远不会执行到此
       System.out.println("你好世界!");//此行报错Unreachable statement遥不可及的声明
    }
}

只有一种情况finally不会执行,退出JVM!

程序示例:

public class HelloWorld {
    public static void main(String[] args) {
        try{
            System.out.println("try");
            //退出JVM,finally中的语句就不会执行了,但此操作无意义
            System.exit(0);
        }finally {
            System.out.println("finally");
        }
    }
}

面试题:该题解释不了,记住就行

public class HelloWorld {
    public static void main(String[] args) {
        /*
        Java语法规则:
        方法体中的代码必须遵循自上而下顺序依次逐行执行
        return语句一旦执行,整个方法必须结束
        */
        System.out.println(m());//100
    }
    public static int m(){
        int i = 100;
        try{
            return i;
        }finally {
            i++;
        }
    }
}
/*
反编译后的代码:
public static int m(){
    int i = 100;
    int j =i;
    i++;
    return j;
}
*/

当finally子句中包含return语句时,可能会产生意想不到的效果。

finally语句只要是用于清理资源,不要把改变控制流的语句(return,throw,break,continue)放在finally中!

public static int parseInt(String s){
    try{
        return Integer.parseInt(s);
    }finally {
        return 0;//Error
    }
}
/*
调用parseInt("42");时,按理说try会返回整数42,不过该方法真正返回前,会执行finally子句,这会导致最后结果返回0
更严重的是调用parseInt("42");时,Integer.parseInt(s)会报NumberFormatException异常,然后执行finally子句,return语句甚至会吞掉该异常
*/

4.7 try-with-resources语句

  • 凡是实现了AutoCloseable接口的类,在try()里声明该类实例的时候,在try结束后,close()方法都会被调用。
  • JDK1.7新增try-with-resources语句
  • try-with-resources语句也可以有catch子句,甚至可以有一个finally子句,这些子句都会在关闭资源后执行。

关闭单一资源程序实例:

public class HelloWorld {
    public static void main(String[] args) {
        try(Resource res = new Resource()) {
            res.doSome();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

class Resource implements AutoCloseable {
    void doSome() {
        System.out.println("do something");
    }
    @Override
    public void close() throws Exception {
        System.out.println("resource is closed");
    }
}
/*
do something
resource is closed
*/

关闭多个资源程序实例:

public class HelloWorld {
    public static void main(String[] args) {
        try(ResourceSome some = new ResourceSome();
            ResourceOther other = new ResourceOther()) {
            some.doSome();
            other.doOther();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

class ResourceSome implements AutoCloseable {
    void doSome() {
        System.out.println("do something");
    }
    @Override
    public void close() throws Exception {
        System.out.println("some resource is closed");
    }
}

class ResourceOther implements AutoCloseable {
    void doOther() {
        System.out.println("do other things");
    }
    @Override
    public void close() throws Exception {
        System.out.println("other resource is closed");
    }
}
/*
do something
do other things
other resource is closed
some resource is closed
*/

4.8 异常对象的常用重要的方法

String msg = exception.getMessage();//获取异常简单的描述信息

String msg = exception.getClass().getName();//获取异常对象的实际类型

exception.printStackTrace();//打印异常追踪的堆栈信息

程序示例:


public class HelloWorld {
    public static void main(String[] args) {
	//此处仅new了异常对象,没有抛出,因此JVM会认为这仅为一个普通对象
        NullPointerException e =new NullPointerException("空指针异常喽!");
	//打印异常简单描述信息
        System.out.println(e.getMessage());
	//打印异常对象的实际类型
        System.out.println(e.getClass().getName());
	//打印异常堆栈信息
        e.printStackTrace();//返回值是void类型,不能sout输出
	System.out.println("你好世界!");//打印异常堆栈追踪信息时涉及异步线程,因此输出顺序可能颠倒
    }
}
/*
空指针异常喽!
java.lang.NullPointerException
你好世界!
java.lang.NullPointerException: 空指针异常喽!
	at Test01.main(Test01.java:9)
*/

JDK9新增StackWalker类,他可以生成一个StackWalker.StackFrame实例流,在其中每个实例分别描述一个栈帧。

public class Test01 {
    public static void main(String[] args){
        foo();
    }
    private static void foo(){
        bar();
    }
    private static void bar(){
        StackWalker stack = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
        stack.forEach(System.out::println);
    }
}
/*
Main.bar(Main.java:10)
Main.foo(Main.java:6)
Main.main(Main.java:3)
*/

4.9 自定义异常

Sun公司提供的异常肯定是不够用的,在实际开发中很多业务出现异常JDK中是没有的。

如何自定义异常?

  1. 定义一个类继承Exception或者继承Exception的某个子类
  2. 提供两个构造方法,一个无参,一个带String参数的

程序示例:

public class MyException extends Exception{//编译时异常
    public MyException(){}
    public MyException(String s){
        super(s);
    }
}
//class MyException extends RuntimeException{//运行时异常
//    public MyException(){}
//    public MyException(String s){
//        super(s);
//    }
//}

如何抛出异常?

使用throw关键字throw new MyException("异常发生!");来代替sout输出

4.10 final finally finalize的区别

  • final:是一个关键字,final修饰的类无法继承;final修饰的方法无法覆盖;final修饰的变量不能重新赋值
  • finally:是一个关键字,和try一起使用;finally语句块中的代码是一定会执行的
  • finalize:是一个标识符,是一个Object类中的方法,是垃圾回收器负责调用的

五、 集合

5.1 集合概述

什么是集合?有什么用?

集合就是一个容器。可以用来容纳其他类型的数据。数组其实就是一个集合。

集合在开发中使用较多。

image.png

集合也是一个对象,也有内存地址,集合里也能存集合

image.png

image.png

image.png

集合在Java JDK哪个包下?

image.png

集合的继承结构

image.png

image.png

image.png

image.png

Map集合继承结构

image.png

image.png

image.png

image.png

image.png

image.png

Collection接口中常用方法

add()

image.png

image.png

image.png

image.png

image.png

image.png

Collection集合迭代

image.png

image.png

迭代器是通用的

image.png

image.png

contains方法解析

image.png

image.png

image.png

contains源码分析

image.png

image.png

image.png

remove源码分析

重写equals方法就会删掉,不重写就不会删掉

image.png

image.png

关于集合中元素的删除

image.png

image.png

image.png

image.png

List接口特有方法

image.png

image.png

image.png

image.png

image.png

ArrayList集合初始化容量以及扩容

image.png

image.png

image.png

image.png

ArrayList集合另一个构造方法

image.png

image.png

image.png

单向链表数据结构

image.png

image.png

image.png

image.png

image.png

image.png

image.png

链表的优点和缺点

image.png

image.png

LinkedList集合源码分析

image.png

image.png

image.png

LinkedList总结

image.png

Vector集合源码分析

image.png

image.png

泛型机制

六、 IO流(未完待续)

七、 多线程(未完待续)

八、 反射机制(未整理)

8.1 反射的概述

8.1.1 什么是反射

​ Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制

8.1.2 反射机制有什么用?

通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件)
通过反射机制可以操作代码片段(class文件)

8.1.3 反射机制相关的类在哪?

反射机制相关的类都在 java.lang.reflect.*

8.1.4 反射机制相关的主要的类有哪些?

  • java.lang.Class:代表整个字节码,代表一个类型,代表整个类
  • java.lang.reflect.Field:代表字节码中的属性字节码。代表类中的成员变量(静态+实例)
  • java.lang.reflect.Method:代表字节码中的方法字节码。代表类中的方法
  • java.lang.reflect.Constructor:代表字节码中的构造方法字节码。代表类中的构造方法
//Class  
public class User{
    // Field
    int no;
    // Constructor
    public User(){}
    // Constructor
    public User(int no){
        this.no = no;
    }
    // Method
    public void setNo(int no){
        this.no = no;
    }
    // Method
    public int getNo(){
        return no;
    }
}

8.2 获取Class的三种方式

要操作一个类的字节码,需要首先获取到这个类的字节码

获取java.lang.Class实例有三种方式:

  1. Class c = Class.forName("完整类名带包名");
  2. Class c = 对象.getClass();
  3. Class c = 任何类型.class;

第一种方式:forName()方法

Class c = Class.forName("完整类名带包名");

java.lang.Class.forName(String className)方法

  • 静态方法,返回值为Class
  • 方法参数为String字符串类型
  • 字符串需要完整类名(必须带有包名,java.lang也不能省)
  • 此方法会导致类加载

第二种方式:getClass()方法

Class c = 对象.getClass();

java.lang.Object.getClass()方法

  • Object类有getClass()方法,因此其他任何一个对象都存在此方法

第三种方式:类型.class

Class c = 任何类型.class;
  • java语言中任何一种类型,包括基本数据类型,它都有.class属性

三种获取类的代码示例:

package com.zzl.reflect;

import java.util.Date;

public class ReflectTest01 {
    public static void main(String[] args) {
        /*
        第一种方法
        CLass.forName()
         */
      
        Class c1 = null;
        Class c2 = null;

        try {
            c1 = Class.forName("java.lang.String");//c1代表String.class文件,或者说代表String类型
            c2 = Class.forName("java.util.Date");//c2代表Date类型
          
            Class c3 = Class.forName("java.lang.Integer");//c3代表Integer类型
            Class c4 = Class.forName("java.lang.System");//c4代表System类型
          
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        /*
        第二种方法
        Java中任何一个对象都有一个方法:getClass()
        Object类有getClass()方法,所以其他对象都存在此方法
        */
        String s = "abc";
        Class x = s.getClass();//x代表的是String.class字节码文件,x代表String类型
        System.out.println(c1 == x);//true(==判断的是对象的内存地址)

        Date time = new Date();
        Class y = time.getClass();
        System.out.println(c2 == y);//true c2和y变量保存的内存地址都一样,都指向方法区中的字节码文件

        /*
        第三种方法
        Java中任何一种类型,包括基本数据类型,都有.class属性
        */
        Class z = String.class;
        Class k = Date.class;
        Class f = int.class;
        Class e = double.class;
        System.out.println(x == z);//true

    }
}

字节码内存图:

https://www.bilibili.com/video/BV1Rx411876f?p=812&t=488.2

image-20220210101756328

8.3 获取Java类能干什么?

通过反射实例化对象

java.lang.Class.newInstance()方法

  • 该方法会使用对应类的无参构造方法来创建该类的实例
  • 该方法要求该 Class 对应类有无参构造方法

通过Class的newInstance()方法来实例化对象

User类

package com.zzl.bean;

public class User {
    public User() {
        System.out.println("无参构造方法执行");
    }
    public User(String name) {
        System.out.println("有参构造方法执行");
    }
}
package com.zzl.reflect;
/*
*使用反射机制创建对象
* */
public class ReflectTest02 {
    public static void main(String[] args) {
      
        try {
            //通过反射机制,获取Class,通过Class来实例化对象
            Class c = Class.forName("com.zzl.bean.User");//c代表User类型
          
            //newInstance()方法会调用User这个类的无参构造方法,完成对象的创建
            //必须保证无参构造存在,否则会抛出实例化异常InstantiationException
            Object obj = c.newInstance();
            System.out.println(obj);
            /*
            无参构造方法执行
            com.zzl.bean.User@1b6d3586
            */
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

8.4 为什么要用反射机制创建对象

不使用反射机制同样可以创建对象,但是为什么还要用反射机制创建呢?

因为反射实例化对象更灵活

8.4.1 验证反射机制的灵活性

reflect\classinfo.properties配置文件

className=com.zzl.bean.User
package com.zzl.reflect;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Properties;

/*
验证反射机制的灵活性
Java代码写一遍,在不改变源代码的基础上,可以做到不同对象的实例化
非常灵活,符合OCP原则:对扩展开放,对修改关闭

后期要学习高级框架ssh、ssm:
高级框架底层实现原理都采用了反射机制,学习反射有利于理解剖析框架底层源代码

 */
public class ReflectTest03 {
    public static void main(String[] args) throws Exception {

        //User user = new User();这种方式代码就写死了,只能创建一个User类

      
        //以下代码是灵活的,代码无需改动,可以只修改配置文件,创建出不同的实例化对象
        //通过IO流读取classinfo.properties文件
        FileReader reader = new FileReader("reflect/classinfo.properties");
        //创建属性类对象Map
        Properties pro = new Properties();//key value 都是String类型
        //加载
        pro.load(reader);
        //关闭流
        reader.close();

        //通过key获取value
        String className = pro.getProperty("className");

        //通过反射实例化对象
        Class c = Class.forName(className);
        Object obj = c.newInstance();
        System.out.println(obj);
        /*
        * 无参构造方法执行
        * com.zzl.bean.User@1b6d3586
        * */
    }
}

8.4.2 研究Class.forName()方法发生了什么?

package com.zzl.reflect;
/*
研究下 Class.forName()发生了什么?

如果仅希望一个类的静态代码块执行,其他代码一律不执行,
可以使用 Class.forName("完整类名")
此方法执行会导致类加载,类加载时,静态代码块会执行
在这里对该方法的返回值不感兴趣,只是为了使用类加载动作
 */
public class ReflectTest04 {
    public static void main(String[] args) {
        try {
            //Class.forName()方法执行会导致:类加载
            Class.forName("com.zzl.reflect.MyClass");
            /*
            MyClass类的静态代码块执行了!
             */
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
class MyClass{
    //静态代码块在类加载时执行,并且只执行一次
    static {
        System.out.println("MyClass类的静态代码块执行了!");
    }
    public MyClass() {//不会执行
        System.out.println("MyClass类无参构造执行了!");
    }
}

8.4.3 研究文件路径问题

image-20220210122359115

package com.zzl.reflect;

import java.io.FileNotFoundException;
import java.io.FileReader;

/*
研究一下文件路径的问题
怎么获取一个文件的绝对路径,下面的方式是通用的。但是有个前提:文件必须在类路径下。
 */
public class AboutPath {
    public static void main(String[] args) throws Exception {
        //以下方式的路径缺点是移植性差,在IDEA中默认的当前路径是project的根
        //代码离开IDEA换到其他位置就会出问题
        //FileReader reader = new FileReader("reflect/classinfo.properties");

      
        //以下方式比较通用,即使代码换位置
        //前提是这个文件必须在类路径下:即在src下
        //src是类的根路径

        /*
        解释
        Thread.currentThread()当前线程对象
        getContextClassLoader()是线程对象的方法,可以获取到当前线程的类加载器对象
        getResource() 获取资源,这是类加载器的方法,当前线程的类加载器默认从类的根路径下加载资源
         */

        String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath();
        //采用以上代码可以拿到一个文件的绝对路径
        System.out.println(path);
        /*
        * /C:/Users/Spring/IdeaProjects/JaveSE/out/production/reflect/classinfo2.properties
        * */

        //获取db.properties文件的绝对路径(从类的根路径作为起点开始)
        String path2 = Thread.currentThread().getContextClassLoader()
                .getResource("com/zzl/bean/db.properties").getPath();
        System.out.println(path2);
        /*
        * /C:/Users/Spring/IdeaProjects/JaveSE/out/production/reflect/com/zzl/bean/db.properties
        * */
    }
}

直接以流的方式返回文件的绝对路径

package com.zzl.reflect;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.InputStream;
import java.util.Properties;

public class IoPropertiesTest {
    public static void main(String[] args) throws Exception {

        //获取一个文件的绝对路径
//        String path = Thread.currentThread().getContextClassLoader()
//                .getResource("classinfo2.properties").getPath();
//        FileReader reader = new FileReader(path);

        //直接以流的方式返回文件绝对路径
        InputStream reader = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("classinfo2.properties");

        //创建属性类对象Map
        Properties pro = new Properties();//key value 都是String类型
        //加载
        pro.load(reader);
        //关闭流
        reader.close();

        //通过key获取value
        String className = pro.getProperty("className");
        System.out.println(className);//com.zzl.bean.User

    }
}

8.5 资源绑定器

java.util包下提供了一个资源绑定器,便于获取属性配置文件的内容

  • 只能绑定xxx.properties文件
  • 这个文件必须在类路径下,以src为起点
  • 不能写上配置文件扩展名

db.properties配置文件

className=java.lang.String
package com.zzl.reflect;

import java.util.ResourceBundle;

/*
java.util包下提供了一个资源绑定器,便于获取属性配置文件的内容
使用以下方式时,属性配置文件xxx.properties必须放到类路径下,都是以src为起点
 */
public class ResourceBundleTest {
    public static void main(String[] args) {

        //资源绑定器,只能绑定xxx.properties文件。并且这个文件必须在类路径下
        //文件拓展名也必须为properties,在写路径时不能写上扩展名
        //ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");
      
        //获取db.properties配置文件内的className的值
        ResourceBundle bundle = ResourceBundle.getBundle("com/zzl/bean/db");
        String className = bundle.getString("className");
        System.out.println(className);//java.lang.String
    }
}

8.6 类加载器概述(了解)

假设有这样一段代码:

String s = "abc";

​ 代码在开始执行之前,会将所需要类全部加载到JVM当中

​ 通过类加载器加载,看到以上代码类加载器会找String.class文件,找到就加载,那么是怎么进行加载的呢?

  1. 首先通过“启动类加载器”加载

    启动类加载器专门加载:C:\Program Files\Java\jdk1.8.0_101\jre\lib\rt.jar

    rt.jar中都是JDK最核心的类库。

  2. 如果通过“启动类加载器”加载不到的时候,会通过"扩展类加载器"加载

    扩展类加载器专门加载:C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext*.jar

  3. 如果“扩展类加载器”没有加载到,那么会通过“应用类加载器”加载

    应用类加载器专门加载:classpath中的类

双亲委派机制

java中为了保证类加载的安全,使用了双亲委派机制

优先从启动类加载器中加载,这个称为“父”。“父”无法加载到,再从扩展类加载器中加载,这个称为“母”。

双亲委派机制:如果都加载不到,才会考虑从应用类加载器中加载。直到加载到为止。

8.7 获取Field(了解)

Student类

package com.zzl.bean;

//反射属性Field

public class Student {
    //Field翻译为字段,其实就是属性/成员
    //四个Field,分别采用不同的访问控制权限修饰符
  
    public int no;//Field对象
    private String name;//Field对象
    protected int age;//Field对象
    boolean sex;//Field对象
    private static final double MATH_PI = 3.1415926;
}
package com.zzl.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/*
反射Student类中的所有Field
 */
public class ReflectTest05 {
    public static void main(String[] args) throws ClassNotFoundException {
        //获取整个类
        Class studentClass= Class.forName("com.zzl.bean.Student");
        System.out.println("完整类名" + studentClass.getName());//完整类名com.zzl.bean.Student
        System.out.println("简单类名" + studentClass.getSimpleName());//简单类名Student

        //获取类中所有public修饰的Field
        Field[] fields = studentClass.getFields();
        System.out.println(fields.length);//1
        Field f = fields[0];
        String fieldName = f.getName();
        System.out.println(fieldName);

        System.out.println("----------------分割线-------------------------");
      
        //获取所有的Field
        Field[] fs = studentClass.getDeclaredFields();//Class.getDeclaredFields()返回字段对象的数组
        System.out.println(fs.length + "个");//5个
        System.out.println("--------");
        for (Field field:fs){
            //获取属性名字
            System.out.println("属性名为" + field.getName());
            //获取属性类型
            Class fieldType = field.getType();//Field.getType()返回类对象,该对象标识此字段对象表示的字段的声明类型
            String fName = fieldType.getName();//name为String类型,想返回简单名字可以用getSimpleName()方法
            System.out.println("类型为" + fName);
            //获取属性的修饰符列表
            int i = field.getModifiers();//因为修饰符列表可能不仅一个,所以有s.返回值为int
            //Field.getModifiers()以整数形式返回此字段对象表示的字段的Java语言修饰符
            System.out.println("修饰符代号为"+i);//返回的是修饰符的代号
          
            String modifierString = Modifier.toString(i);
            //java.lang.reflect.Modifier.toString(int mod)返回描述指定修饰符中的访问修饰符标志的字符串
          
            System.out.println("修饰符列表为" + modifierString);
            System.out.println("--------");
     	
        }
    }
}
/* 完整结果:

完整类名com.zzl.bean.Student
简单类名Student
1
no
----------------分割线-------------------------
5个
--------
属性名为no
类型为int
修饰符代号为1
修饰符列表为public
--------
属性名为name
类型为java.lang.String
修饰符代号为2
修饰符列表为private
--------
属性名为age
类型为int
修饰符代号为4
修饰符列表为protected
--------
属性名为sex
类型为boolean
修饰符代号为0
修饰符列表为
--------
属性名为MATH_PI
类型为double
修饰符代号为26
修饰符列表为private static final
--------
/*

8.8 反编译Field(了解)

package com.zzl.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

//通过反射机制,反编译一个类的属性Field
public class ReflectTest06 {
    public static void main(String[] args) throws Exception {
        //创建这个字符串只是为了拼接字符串
        StringBuilder s = new StringBuilder();
      
        //获取类
        Class studentClass = Class.forName("com.zzl.bean.Student");
        //Class studentClass = Class.forName("java.lang.String");

        s.append(Modifier.toString(studentClass.getModifiers()) + " class " + studentClass.getSimpleName() + "{\n");

        Field[] fields = studentClass.getDeclaredFields();
        for(Field field:fields){
            s.append("\t");
            s.append(Modifier.toString(field.getModifiers()));
            s.append(" ");
            s.append(field.getType().getSimpleName());
            s.append(" ");
            s.append(field.getName());
            s.append(";\n");
        }
        s.append("}");
        System.out.println(s);
        /*
        public class Student{
            public int no;
            private String name;
            protected int age;
             boolean sex;
            private static final double MATH_PI;
}
         */

    }
}

8.9 通过反射机制访问对象的某个属性(重点掌握)

package com.zzl.reflect;

import com.zzl.bean.Student;

import java.lang.reflect.Field;

/*
必须掌握:
怎么通过反射机制访问一个Java对象的属性?
    给属性赋值set
    获取属性的值get
 */
public class ReflectTest07 {
    public static void main(String[] args) throws Exception {
        //不使用反射如何访问属性的值
        /*Student s = new Student();
        s.no = 10;
        System.out.println(s.no);*/

        //使用反射机制,怎么去访问一个对象的属性
        Class studentClass = Class.forName("com.zzl.bean.Student");
        Object obj = studentClass.newInstance();//此时obj就是Student对象,底层调用了无参构造方法

        //获取属性靠名字区分
        //获取no属性
        Field noField = studentClass.getDeclaredField("no");
        //Class.getDeclaredField(String name)方法可以获取指定属性
      
        //给obj对象的no属性赋值
        /*
        虽然使用了反射机制,但是三要素缺一不可
            1.obj对象
            2.no属性
            3.值
        注意:反射机制让代码复杂了,但是为了"灵活"值得!
        实际开发中写反射机会很少
        */
        noField.set(obj,11);//Field.set(Object obj, Object value)将obj类的noField属性赋值为value
        //读obj的值
        //两个要素:获取obj对象的no属性值
        System.out.println(noField.get(obj));//noField.get(obj)得到obj类的noField属性的值,返回值为Object类型,直接输出11

      
        //可以访问私有属性吗?
        //获取name属性
        Field nameField = studentClass.getDeclaredField("name");
      
        //打破封装:不打破封装访问不了,抛出IllegalAccessException
        //打破封装会给不法分子留下机会
        //打破封装后,在外部也可以访问private
        nameField.setAccessible(true);
        //Field.setAccessible(boolean flag) 将此反射对象的 accessible标志设置为指示的布尔值
      
        //给name属性赋值
        nameField.set(obj,"John");
        //读取输出name值
        System.out.println(nameField.get(obj));
    }

}

8.10 可变长度参数

  1. 可变长度参数要求参数个数为0到n
  2. 可变长度参数在参数列表中必须在最后,且只能有一个
  3. 可变长度参数可以当数组来看待
package com.zzl.reflect;

/*
可变长度参数
    int ... args 这就是可变长度参数
    语法是... (一定是三个点!)
* */
public class ArgsTest {
    public static void main(String[] args) {
        m();
        m(10);
        m(10,20);
        //m("abc");必须为int类型

        m1(10,"abc","def");
        m2("aa","bb","cc");
        String[] strs = {"a","b","c"};
        //也可以传一个数组
        m2(strs);
        m2(new String[]{"ni","hao"});//没必要,直接以下就行
        m2("ni","hao");
        /*
        1.可变长度参数要求参数个数为0到n
        2.可变长度参数在参数列表中必须在最后,且只能有一个
        3. 可变长度参数可以当数组来看待
        * */
    }
    public static void m(int... args){//可0可n
        System.out.println("m方法执行了");
    }
    public static void m1(int a,String... args1){//可1可n
        System.out.println("m1方法执行了");
    }
    public static void m2(String... args){
        //args竟然有length属性,说明args是一个数组

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

8.11 反射Method

UserService类

package com.zzl.service;
/*
    用户业务类
* */
public class UserService {
    int no;
    int age;

    public boolean login(String name,String password){
        if("admin".equals(name)&&"123".equals(password)){
            System.out.println("登录成功");
            return true;
        }
        return false;
    }

    public void login(int i){
    }

    public void logout(){
        System.out.println("系统已经安全退出!");
    }
}

package com.zzl.reflect;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/*
作为了解内容:反射Method
* */
public class ReflectTest08 {
    public static void main(String[] args) throws Exception {
        //获取类
        Class userServiceClass = Class.forName("com.zzl.service.UserService");

        //获取所有的Method(包括私有的)
        Method[] methods = userServiceClass.getDeclaredMethods();
        System.out.println("方法个数有" + methods.length);//2
        //遍历Method
        System.out.println("--------");
        for(Method method:methods){
            //获取修饰符列表
            System.out.println("修饰符列表" + Modifier.toString(method.getModifiers()));
            //获取方法的返回值类型
            System.out.println("返回值类型为" + method.getReturnType());
            //获取方法名
            System.out.println("方法名为" + method.getName());
            //获取方法中的参数
            Class[] parameterTypes = method.getParameterTypes();
            for(Class parameterType:parameterTypes){
                //获取参数类型
                System.out.println("参数类型为" + parameterType.getSimpleName());
            }
            System.out.println("--------");

        }
    }
}

8.12 反编译Method

package com.zzl.reflect;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/*
了解一下,不需要掌握(反编译一个类的方法。)
 */
public class ReflectTest09 {
    public static void main(String[] args) throws Exception{
        StringBuilder s = new StringBuilder();
        Class userServiceClass = Class.forName("com.zzl.service.UserService");
//        Class userServiceClass = Class.forName("java.lang.String");
        s.append(Modifier.toString(userServiceClass.getModifiers()) + " class "+userServiceClass.getSimpleName()+" {\n");

        Method[] methods = userServiceClass.getDeclaredMethods();
        for(Method method : methods){
            //public boolean login(String name,String password){}
            s.append("\t");
            s.append(Modifier.toString(method.getModifiers()));
            s.append(" ");
            s.append(method.getReturnType().getSimpleName());
            s.append(" ");
            s.append(method.getName());
            s.append("(");
            // 参数列表
            Class[] parameterTypes = method.getParameterTypes();
            for(Class parameterType : parameterTypes){
                s.append(parameterType.getSimpleName());
                s.append(",");
            }
            if(parameterTypes.length > 0){
                // 删除指定下标位置上的字符
                s.deleteCharAt(s.length() - 1);
            }
            s.append("){}\n");
        }

        s.append("}");
        System.out.println(s);
    }
}

8.13 反射机制调用方法(重点掌握)

package com.zzl.reflect;

import com.zzl.service.UserService;

import java.lang.reflect.Method;

/*
通过反射机制怎么调用一个对象的方法

反射机制让代码具有通用性,可变化的内容都放到配置文件中,将来仅需要修改配置文件,而Java代码保持不变
* */
public class ReflectTest10 {
    public static void main(String[] args) throws Exception {

        //不使用反射调用方法
        /*
        UserService userService = new UserService();
        boolean loginSuccess = userService.login("admin","123");
        System.out.println(loginSuccess?"登录成功":"登录失败");

        调用方法要素分析:
        1.对象
        2.login方法
        3.实参列表
        4.返回值
        */

        //使用反射机制怎么调用方法
        //先获取一个类
        Class userServiceClass = Class.forName("com.zzl.service.UserService");
        //创建对象
        Object obj = userServiceClass.newInstance();

        //获取Method
        //Java中区分一个方法是通过方法名和参数列表
        Method loginMethod = userServiceClass.getDeclaredMethod("login",String.class,String.class);
        //Method loginMethod = userServiceClass.getDeclaredMethod("login",int.class);
        //userServiceClass.getDeclaredMethod("login",String.class,String.class);中后面的参数类型是Class类型

        Object retValue = loginMethod.invoke(obj,"admin","123");//invoke()是调用函数,反射机制中最重要的方法
        System.out.println(retValue);//true


    }
}

8.14 反射Constructor

Vip类

package com.zzl.bean;

/*
反编译一个类的Constructor构造方法
* */
public class Vip {
    int no;
    String name;
    String birth;
    boolean sex;

    public Vip() {
    }

    public Vip(int no) {
        this.no = no;
    }

    public Vip(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public Vip(int no, String name, String birth) {
        this.no = no;
        this.name = name;
        this.birth = birth;
    }

    public Vip(int no, String name, String birth, boolean sex) {
        this.no = no;
        this.name = name;
        this.birth = birth;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Vip{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", birth='" + birth + '\'' +
                ", sex=" + sex +
                '}';
    }
}

package com.zzl.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

public class ReflectTest11 {
    public static void main(String[] args) throws Exception {

        StringBuilder s = new StringBuilder();
        Class vipClass = Class.forName("com.zzl.bean.Vip");
        s.append(Modifier.toString(vipClass.getModifiers()));
        s.append(" class ");
        s.append(vipClass.getSimpleName());
        s.append("{\n");

        // 拼接构造方法
        Constructor[] constructors = vipClass.getDeclaredConstructors();
        for(Constructor constructor : constructors){
            //public Vip(int no, String name, String birth, boolean sex) {
            s.append("\t");
            s.append(Modifier.toString(constructor.getModifiers()));
            s.append(" ");
            s.append(vipClass.getSimpleName());
            s.append("(");
            // 拼接参数
            Class[] parameterTypes = constructor.getParameterTypes();
            for(Class parameterType : parameterTypes){
                s.append(parameterType.getSimpleName());
                s.append(",");
            }
            // 删除最后下标位置上的字符
            if(parameterTypes.length > 0){
                s.deleteCharAt(s.length() - 1);
            }
            s.append("){}\n");
        }

        s.append("}");
        System.out.println(s);
    }
}

8.15 反射机制调用构造方法

package com.zzl.reflect;

import com.zzl.bean.Vip;

import java.lang.reflect.Constructor;

/*
比上一个例子(ReflectTest11)重要一些!!!

通过反射机制调用构造方法实例化java对象。(这个不是重点)
 */
public class ReflectTest12 {
    public static void main(String[] args) throws Exception {
        // 不使用反射机制怎么创建对象
//        Vip v1 = new Vip();
//        Vip v2 = new Vip(110, "zhangsan", "2001-10-11", true);

        // 使用反射机制怎么创建对象呢?
        Class c = Class.forName("com.zzl.bean.Vip");
        // 调用无参数构造方法
        Object obj = c.newInstance();
        System.out.println(obj);

        // 调用有参数的构造方法怎么办?
        // 第一步:先获取到这个有参数的构造方法
        Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);
        // 第二步:调用构造方法new对象
        Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);
        System.out.println(newObj);

        // 获取无参数构造方法
        Constructor con2 = c.getDeclaredConstructor();
        Object newObj2 = con2.newInstance();
        System.out.println(newObj2);

    }
}

8.16 获取父类和父接口

package com.zzl.reflect;

/*
重点:给你一个类,怎么获取这个类的父类,已经实现了哪些接口?
 */
public class ReflectTest13 {
    public static void main(String[] args) throws Exception {
        // String举例
        Class stringClass = Class.forName("java.lang.String");

        // 获取String的父类
        Class superClass = stringClass.getSuperclass();
        System.out.println(superClass.getName());

        // 获取String类实现的所有接口(一个类可以实现多个接口。)
        Class[] interfaces = stringClass.getInterfaces();
        for(Class in : interfaces){
            System.out.println(in.getName());
        }
    }
}

九、 注解(未整理)

注解(Annotation)又叫注释类型

注解是一种引用数据类型。编译后也是生成xxx.class文件

如何定义注解?语法格式

[修饰符列表] @interface 注解类型名{
  
}

注解怎么使用?用在什么地方?

  1. 注解使用时的语法格式是

    @注解类型名
    
  2. 注解还可以出现在类上、属性上、方法上、变量上、接口上等等,注解还可以出现在注解类型上

package com.zzl.annotation;

/*
*自定义注解
*/
public @interface MyAnnotation {

}
package com.zzl.annotation;
/*
* 注解修饰注解
* */
@MyAnnotation
public @interface OtherAnnotation {
}

package com.zzl.annotation;
/*
* 默认情况下注解可以出现在任意位置
* */

@MyAnnotation
public class AnnotationTest01 {

    @MyAnnotation
    private int no;

    @MyAnnotation
    public AnnotationTest01(){}

    @MyAnnotation
    public static void m1(){
        @MyAnnotation
        int i = 100;
    }

    @MyAnnotation
    public void m2(@MyAnnotation String name){}
}

@MyAnnotation
interface MyInterface{}

@MyAnnotation
enum Season{
    SPRING,SUMMER,AUTUMN,WINTER
}

JDK内置了哪些注解?

java.lang包下的注释类型:

  • 掌握:

    • Deprecated用@Deprecated注释的程序元素
    • 不鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择
  • 掌握:

    • Override表示一个方法声明打算重写超类中的另一个方法声明
  • 不用掌握

    • SupperssWarnings指示应该在注释元素(以及包含在该注释元素中的所有程序元素)中取消显示指定的编译器警告

Override注解

package com.zzl.annotation;
/*
* 关于JDK lang包下的Override注解
*源代码为:
* public @interface Override {}
*
* 标识性注解,给编译器作参考的。
* 编译器看到方法上有这个注解的时候,编译器会自动检查该方法是否重写了父类的方法
* 如果没有重写会报错
*
*
*
*
* @Override这个注解只能注解方法
* @Override这个注解是给编译器参考的,和运行阶段没有关系
* 凡是Java中的方法带有这个注解的,编译器都会进行编译检查,如果这个方法不是重写方法,编译器就会报错
*
* */
//@Override不放在类上
public class AnnotationTest02 {

    @Override
    public String toString() {
        return "toString";
    }
}

/*
* @Override注解被以下两个注解所注解:
* @Target(ElementType.METHOD)
* @Retention(RetentionPolicy.SOURCE)
* */

元注解

什么是元注解?

用来标注“注解”的“注解”,称为元注解

常用的元注解有哪些?

Target

Retention

关于Target注解:

这是一个元注解,用来标注“注解类型”的“注解”

这个Target注解用来标注“被标注的注解可以出现在哪些位置上”

@Target(ElementType.METHOD)//这个"被标注的注解"只能出现在方法上
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})//该注解可以出现在:构造方法上、字段上、局部变量上、方法上....

关于Retention注解:

这是一个元注解,用来标注“注解类型”的“注解”

这个Retention注解用来标注“被标注的注解”最终保存到哪里

@Retention(RetentionPolicy.SOURCE)//表示该注解只被保留在Java源文件中
@Retention(RetentionPolicy.CLASS)//表示该注解被保留在class文件中
@Retention(RetentionPolicy.RUNTIME)//表示该注解被保留在class文件中,并且可以被反射机制所读取

@Deprecated注解

package com.zzl.annotation;
/*
* @Deprecated
* 表示标注的元素已经过时
* 这个注解主要是向其他程序员传达一个信息,告知已过时,有更好的解决方案
* 编译器可以捕捉到过时的东西并且Warning,@Deprecated说明不仅保存在Java源文件中
* */

public class AnnotationTest03 {
    public static void main(String[] args) {
        AnnotationTest03 at = new AnnotationTest03();
        at.doSome();
    }
    @Deprecated//表示这个方法已过时
    public void doSome(){
        System.out.println("doSome!");
    }
    @Deprecated//表示这个方法已过时
    public static void doOther(){
        System.out.println("doOther!");
    }
}
class T {
    public static void main(String[] args) {
        AnnotationTest03 at = new AnnotationTest03();
        at.doSome();//doSOme()已过时
        AnnotationTest03.doOther();//doOther()已过时
    }
}

注解中定义属性

package com.zzl.annotation2;

public @interface MyAnnotation {

    //我们通常在注解中可以定义属性
    //以下是MyAnnotation的name属性
    //看着像一个方法,实际上称为属性name
    String name();

    String color();
    int age() default 25;//属性指定值后,使用注解时就可以不写此属性了
}
package com.zzl.annotation2;

public class MyAnnotationTest {

    //报错原因,如果一个注解中有属性,那么必须给属性赋值
    //除非该属性使用default指定了默认值

    //@MyAnnotation(属性名=属性值)
    @MyAnnotation(name = "zhangsan",color = "red")
    @Deprecated()
    public void doSome(){


    }
}

属性名为value时可以省略属性名

package com.zzl.annotation3;

public @interface MyAnnotation {
    //指定一个value属性
    String value();

//    String name();
}
package com.zzl.annotation3;

public class MyAnnotationTest {

    //如果一个属性名为value且仅有value一个属性时(包括value[]),在使用时该属性名可以省略,仅写属性值
    //如果有value和其他属性,那么就都必须写好属性名
    @MyAnnotation("haha")
    public void doSOme(){

    }
}

注解中属性可以是哪一种类型?

package com.zzl.annotation4;

public @interface MyAnnotation {

    /*
    * 注解中属性可以是哪一种类型?
    *  byte short int long float double boolean char String Class 枚举类
    *  以及以上每一种的数组形式
    * */
    int value1();
    String value2();
    int[] value3();
    String[] value4();
    Class parameterType();
    Class[] parameterTypes();
}

当属性是一个数组时

package com.zzl.annotation4;

public @interface OtherAnnotation {
    int age();
    String[] email();
    Season[] seasonArray();
}

package com.zzl.annotation4;

public enum Season {
    SPRING,SUMMER,AUTUMN,WINTER
}
package com.zzl.annotation4;

public class OtherAnnotationTest {


    @OtherAnnotation(age = 25, email = {"zhangsan@qq.com","lisi@sohu.com"},seasonArray = {Season.SPRING,Season.SUMMER})
    public void doSome(){

    }

    //当数组中只有一个元素,大括号可以省略
    @OtherAnnotation(age = 25, email = "zhangsan@qq.com",seasonArray = Season.SPRING)
    public void doOther(){

    }
}

源代码分析

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})

//@Target中有且仅有一个属性为value数组,且数组元素类型为枚举类型
//value数组也可以省略属性名使用,此处未省略

public @interface Deprecated {
}

源代码分析

@Documented
@Retention(RetentionPolicy.RUNTIME)

//@Retention中有且仅有一个属性为value,且属性值为枚举类型


@Target(ElementType.ANNOTATION_TYPE)

//@Target中有且仅有一个属性为value数组,且数组元素类型为枚举类型


public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

反射注解

package com.zzl.annotation5;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//只允许该注解标注类、方法
@Target({ElementType.TYPE,ElementType.METHOD})
//希望该注解可以被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    String value() default "北京市";
}

package com.zzl.annotation5;

@MyAnnotation
public class MyAnnotationTest {

    //@MyAnnotation
    int i;

    //@MyAnnotation不可以出现在构造方法上
    public MyAnnotationTest() {
    }

    @MyAnnotation
    public void doSome(){
        //@MyAnnotation
        int i;
    }
}

package com.zzl.annotation5;


//获取类上的注解
public class ReflectAnnotationTest {
    public static void main(String[] args) throws Exception {
        //获取类
        Class c = Class.forName("com.zzl.annotation5.MyAnnotationTest");
        //判断类上是否有@MyAnnotation
        //System.out.println(c.isAnnotationPresent(MyAnnotation.class));//true


        //如果注解仅保存在Java源文件中,反射机制抓取不到,if()条件不成立
        if(c.isAnnotationPresent(MyAnnotation.class)){//如果有注解就获取到
            //获取该注解对象
            MyAnnotation myAnnotation = (MyAnnotation) c.getAnnotation(MyAnnotation.class);
            System.out.println("类上面的注解对象" + myAnnotation);
            //获取注解对象的属性怎么办?和调接口没区别
            String value = myAnnotation.value();//北京市
            System.out.println(value);

        }

        //判断String类上是否有@MyAnnotation
        Class stringClass = Class.forName("java.lang.String");
        //判断类上是否有@MyAnnotation
        System.out.println(stringClass.isAnnotationPresent(MyAnnotation.class));//false
    }

}

通过反射机制获取注解对象属性的值

package com.zzl.annotation6;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String username();
    String password();
}
package com.zzl.annotation6;


/*
* 通过反射机制获取注解对象属性的值
* */

import java.lang.reflect.Method;

public class MyAnnotationTest {
    @MyAnnotation(username = "admin",password = "123")
    public void doSome(){

    }

    public static void main(String[] args) throws Exception {
        //获取MyAnnotationTest的doSome()方法上的注解信息
        Class c = Class.forName("com.zzl.annotation6.MyAnnotationTest");
        //获取doSome()方法
        Method doSomeMethod = c.getDeclaredMethod("doSome");
        //判断该方法上是否存在这个注解
        if(doSomeMethod.isAnnotationPresent(MyAnnotation.class)){
            //拿到这个注解
            MyAnnotation myAnnotation = doSomeMethod.getAnnotation(MyAnnotation.class);
            System.out.println(myAnnotation.username());
            System.out.println(myAnnotation.password());

        }

    }
}

注解在实际开发中有什么用?

需求:

​ 假设有这样一个注解,叫做:@id

​ 这个注解只能出现在类上面,当这个类有这个注解时,要求这个类必须有一个int类型的id属性。如果没有这个属性就报异常。如果有这个属性就正常进行

package com.zzl.annotation7;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//表示这个注解只能出现在类上面
@Target(ElementType.TYPE)
//该注解可以被反射机制读取到
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {
    /*
    * 这个注解只能出现在类上面,当这个类有这个注解时,要求这个类必须有一个int类型的id属性。如果没有这个属性就报异常
    * */


}

package com.zzl.annotation7;


@Id
public class User {
    int id;
    String name;
    String password;

}

package com.zzl.annotation7;

import java.lang.reflect.Field;

public class Test {

    public static void main(String[] args) throws Exception {
        //获取类
        Class userClass = Class.forName("com.zzl.annotation7.User");

        boolean isOk = false;//给一个默认的标记
        //判断类上是否有Id注解
        if(userClass.isAnnotationPresent(Id.class)){
            //获取类的属性
            Field[] fields = userClass.getDeclaredFields();
            for(Field field:fields){
                if("id".equals(field.getName())&&"int".equals(field.getType().getSimpleName())){
                    isOk = true;//表示合法
                    break;
                }
            }

            //判断是否合法
            if(!isOk){
                throw new HasNotIdPropertyException("被@Id注解标注的中中必须有一个it类型的属性");
            }
        }


    }



}

posted @ 2022-02-12 13:51  Spring୧⍤⃝✨  阅读(59)  评论(0)    收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css