[Java基本语法] 从0到1带你精通Java基本语法

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述
✒️ 今天开始,Java基本语法模块将进行重构.以保证文章质量.

目录

  • 1. 准备工作
    • 1.1 安装idea
    • 1.2 配置环境变量与安装jdk
  • 2. 前置知识
    • 2.1 Java的main方法
    • 2.2 Java程序编写中的常见错误
    • 2.3 Java程序是如何运行起来的
    • 2.4 注释
      • 2.4.1 基本规则
      • 2.4.2 注释规范
    • 2.5 标识符
    • 2.6 关键字
  • 3. 数据类型与变量
    • 3.1 字面常量
    • 3.2 数据类型
    • 3.3 变量
      • 3.3.1 相关概念
      • 3.3.2 整形变量
      • 3.3.3 长整形变量
      • 3.3.4 短整型变量
      • 3.3.5 字节类型变量
      • 3.3.6 浮点类型变量
        • 3.3.6.1 双精度浮点数
        • 3.3.6.2 单精度浮点数
      • 3.3.7 字符类型变量
      • 3.3.8 布尔类型变量
      • 3.3.9 类型转换
        • 3.3.9.1 自动类型转换
        • 3.3.9.2 强制类型转换
      • 3.3.10 整形提升
      • 3.3.11 字符串类型
  • 4. 运算符
    • 4.1 算数运算符
    • 4.2 关系运算符
    • 4.3 逻辑运算符
    • 4.4 位运算符
    • 4.5 移位运算符
    • 4.6 条件运算符(三目运算符)
  • 5. 逻辑控制
    • 5.1 顺序结构
    • 5.2 分支结构
      • 5.2.1 if语句
      • 5.2.2 switch语句
    • 5.3 循环结构
      • 5.3.1 while循环
      • 5.3.2 break
      • 5.3.3 continue
      • 5.3.4 for循环
    • 5.4 输入与输出
      • 5.4.1 输出到控制台
      • 5.4.2 从键盘输入
  • 6. 方法的使用
    • 6.1 方法的概念与使用
      • 6.1.1 什么是方法
      • 6.1.2 方法的定义
      • 6.1.3 方法调用的执行过程
      • 6.1.4 实参和形参的关系
    • 6.2 方法的重载
      • 6.2.1 方法重载的概念
      • 6.2.2 方法签名
    • 6.3 递归
      • 6.3.1 递归的概念
      • 6.3.2 递归执行过程分析
  • 7. 类与对象
    • 7.1 如何创建类与对象
    • 7.2 this引用
      • 7.2.1 为什么要用this引用
      • 7.2.2 何为this引用
      • 7.2.3 this引用的特性
    • 7.3 对象的构造及其初始化
      • 7.3.1 如何初始化对象
      • 7.3.2 构造方法
        • 7.3.2.1调用构造方法的快捷键
        • 7.3.2.2概念
        • 7.3.2.3特性
      • 7.3.3 默认初始化
      • 7.3.4 就地初始化
    • 7.4 封装
      • 7.4.1 封装的概念
      • 7.4.2 访问限定符
      • 7.4.3 包
        • 7.4.3.1 包的概念
        • 7.4.3.2 导包
        • 7.4.3.3 自定义包
        • 7.4.3.4 基于包的访问权限
        • 7.4.3.5常见的包
    • 7.5 static成员
      • 7.5.1 为什么要使用static成员
      • 7.5.2 static修饰成员变量
      • 7.5.3 static修饰成员方法
      • 7.5.4 static成员变量的初始化
    • 7.6 代码块
      • 7.6.1 代码块的概念以及分类
      • 7.6.2 普通代码块
      • 7.6.3 构造代码块
      • 7.6.4 静态代码块
      • 7.6.5 代码块执行的顺序
  • 8. 继承与多态
    • 8.1 继承
      • 8.1.1 为什么需要继承
      • 8.1.2 继承的概念
      • 8.1.3 继承的语法格式
      • 8.1.4 父类成员访问
        • 8.1.4.1子类中访问父类的成员变量
        • 8.1.4.2 子类中访问父类的成员方法
      • 8.1.5 super关键字
      • 8.1.6 子类构造方法
      • 8.1.7 super和this
      • 8.1.8 继承体系下的代码块
      • 8.1.9 再谈访问限定符
      • 8.1.10 继承方式
      • 8.1.11 final关键字
      • 8.1.12 继承与组合
    • 8.2 多态
      • 8.2.1 多态的概念
      • 8.2.2 多态实现的条件
      • 8.2.3 重写
      • 8.2.4 向上转型和向下转型
      • 8.2.5 避免在构造方法中使用重写的方法
  • 9. 数组
    • 9.1 数组的认识
      • 9.1.1 数组的基本概念
        • 9.1.1.1 什么是数组
        • 9.1.1.2数组的创建与初始化
        • 9.1.1.3数组的初始化
        • 9.1.1.4数组的使用
      • 9.1.2 组是引用类型
        • 9.1.2.1基本类型和引用类型在底层上的区别
        • 9.1.2.2认识null
      • 9.1.3 数组的使用场景
    • 9.2 数组的应用与常用方法
      • 9.2.1 数组转字符串
      • 9.2.2 数组拷贝
      • 9.2.3 求数组中的平均值
      • 9.2.4 顺序查找数组元素
      • 9.2.5 二分查找数组元素
      • 9.2.6 数组的排序
      • 9.2.7 数组逆序
      • 9.2.8 二维数组
  • 10.抽象类与接口
    • 10.1 抽象类
      • 10.1.1抽象类的概念
      • 10.1.2抽象类的语法
      • 10.1.3抽象类的特性
      • 10.1.4 抽象类的作用
    • 10.2 接口
      • 10.2.1接口的概念
      • 10.2.2语法规则
      • 10.2.3 接口的使用
      • 10.2.4接口特性
      • 10.2.5 实现多个接口
      • 10.2.6 接口之间的继承
      • 10.2.7 接口的经典使用案例
      • 10.2.8抽象类与接口的区别
    • 10.3 拷贝与Object类
      • 10.3.1 Object类
        • 10.3.1.1 概述
        • 10.3.1.2 toString方法
        • 10.3.1.3 对象比较equals方法
      • 10.3.1.4 hashcode方法
      • 10.3.2 Clonable接口与深拷贝浅拷贝
  • 11. String类
    • 11.1 常用方法
      • 11.1.1 字符串构造
      • 11.1.2 字符串的常规方法
      • 11.1.3 String对象的比较
      • 11.1.4 字符串查找
      • 11.1.5 转化
      • 11.1.6 字符串替换
      • 11.1.7 字符串的拆分
      • 11.1.8 字符串截取
      • 11.1.9 大小写转换
      • 11.1.10 字符串的不可变性
      • 11.1.11 字符串的修改
    • 11.2 StringBuilder, StringBuffer与OJ例题
      • 11.2.1 StringBuilder和StringBuffer
      • 11.2.2 String类OJ
  • 12. 异常
    • 12.1 异常的概念与体系结构
      • 12.1.1 异常的概念
      • 12.1.2 异常的体系结构
      • 12.1.3 异常的分类
    • 12.2 异常的处理
      • 12.2.1 防御式编程
      • 12.2.2 异常的抛出
      • 12.2.3 异常的捕获
        • 12.2.3.1 异常throws声明
        • 12.2.3.2 捕获并处理
        • 12.2.3.3 finally
      • 12.2.4 异常处理的流程 (总结)
    • 12.3 自定义异常

1. 准备工作

1.1 安装idea

安装地址: https://www.jetbrains.com/zh-cn/idea/
安装社区版本即可,建议安装2021.3的版本,以便我们后面学习Spring时使用.

1.2 配置环境变量与安装jdk

可参考视频: https://www.bilibili.com/video/BV1N54y1U7AA/?spm_id_from=333.999.0.0&vd_source=f2e28dfb590b0d28f1fc01e22608db8d

2. 前置知识

2.1 Java的main方法

public class HelloWorld{
	public static void main(String[] args){
	System.out.println("Hello,world");
	}
}
  • 在我们的idea中中写main方法的时候,只需要写一个main,会出来一个提示,之后一敲回车就自动帮你写好了.
  • 同样,在idea中写打印拿一行的时候,只写一个sout,也会出来提示,一敲回车,也会自动补全代码.

上面是Java中main方法的写法,可以说是主流编程语言当中最"长"的.从上述代码中,我们可以看到一个完整的Java程序结构由以下三部分组成.
1.(扩展名为*.java):源文件带有类的定义。类用来表示程序的一个组件,小程序或许只会有一个类。类的内容必须包含在花括号里面。
2.类:类中带有一个或多个方法。方法必须在类的内部声明。
3.方法:在方法的花括号中编写方法应该执行的语句。
总结一下:类存在于源文件里面;方法存在于类中;语句存在于方法中
注意:在一个源文件中只能有一个public修饰的类,而且源文件名字必须与public修饰的类名字相同.

2.2 Java程序编写中的常见错误

  1. 源文件名后缀不是.java
  2. 类名与文件名不一致
  3. main方法名字写错:mian
  4. 类没有使用public修饰
  5. 方法中语句没有以分号结尾
  6. 中文格式的分号
  7. JDK环境没有配置好,操作系统不能识别javac或者java命令

2.3 Java程序是如何运行起来的

别看我们在idea中只点一个运行就可以让一个Java程序运行起来,其实在背后做了很多事情.
先通过javac编译程序把源文件进行编译编译后生成的.class文件是由字节码组成的平台无关、面向JVM的文件。最后启动java虚拟机来运行.class文件,此时JVM会将字节码转换成平台能够理解的形式来运行。
在这里插入图片描述

2.4 注释

为代码添加注释,是编程中一个非常好的习惯.这个注释是给别人看的,更是为3个月后的自己看的.写注释的初心就是为了使得代码阅读起来更加方便.

2.4.1 基本规则

Java中的注释主要分为一下三种:

  1. 单行注释://注释内容(用的最多)
  2. 多行注释:/* 注释内容*/
  3. 文档注释: /** 文档注释 */(常见于方法和类之上描述方法和类的作用),可以被javadoc工具解析,生成一套以网页文件形式体现的程序说明文档
/**
 * 多行注释
 */
public class Test {
    /**
     * 文档注释
     * @param args 方法参数
     */
    public static void main(String[] args) {
        System.out.println("hello world");//单行注释
    }
}

2.4.2 注释规范

  1. 内容准确: 注释内容要和代码一致, 匹配, 并在代码修改时及时更新.
  2. 篇幅合理: 注释既不应该太精简, 也不应该长篇大论.
  3. 使用中文: 一般中国公司都要求使用中文写注释, 外企另当别论.

2.5 标识符

在上述程序中,Test称为类名,main称为方法名,也可以将其称为标识符,即:在程序中由用户给类名、方法名或者变量所取的名字

  • 硬性规则
    标识符中可以包含:字母、数字以及 下划线和 $ 符号等等
    注意:标识符不能以数字开头,也不能是关键字,且严格区分大小写
  • 软性建议
    • 类名:每个单词的首字母大写(大驼峰)
    • 方法名:首字母小写,后面每个单词的首字母大写(小驼峰)
    • 变量名:与方法名规则相同

2.6 关键字

关键字是由Java语言提前定义好的,有特殊含义的标识符,或者保留字
注意:用户不能使用关键字定义标识符。
在Java中关键字有很多,这里给大家列出来一部分,先了解下后序在逐一详细解释。
在这里插入图片描述

3. 数据类型与变量

3.1 字面常量

常量即程序运行期间,固定不变的量称为常量,比如:一个礼拜七天,一年12个月等。常量不可以进行修改

public class Demo{
    public static void main(String[] args){
         System.Out.println("hello world!");
         System.Out.println(100);
         System.Out.println(3.14);
         System.Out.println('A');
         System.Out.println(true);
         System.Out.println(false);
   }
}

常用快捷键: Ctrl+d自动复制一行

其中:100、3.14、‘A’、true/false都是常量,将其称为字面常量。
字面常量的分类:

  1. 字符串常量:由""括起来的,比如“12345”、“hello”、“你好”。
  2. 整形常量:程序中直接写的数字(注意没有小数点),比如:100、1000
  3. 浮点数常量:程序中直接写的小数,比如:3.14、0.49
  4. 字符常量:由 单引号 括起来的当个字符,比如:‘A’、‘1’
  5. 布尔常量:只有两种true和false
  6. 空常量:null

3.2 数据类型

在Java中数据类型主要分为两类:基本数据类型和引用数据类型。基本数据类型也叫有包装类,引用数据类型也叫做无包装类.

  • 基本数据类型有四类八种:
  1. 四类:整型、浮点型、字符型以及布尔型
  2. 八种:
    在这里插入图片描述
    [注意事项]
    • 不论是在16位系统还是32位系统,int都占用4个字节,long都占8个字节,所以Java具有较好的可移植性.
    • 整形和浮点型都是带有符号的
    • 整型默认为int型,浮点型默认为double
  • 引用数据类型有:String(字符串类),数组,类,接口.

3.3 变量

3.3.1 相关概念

  • 定义
    在程序中,除了有始终不变的常量外,有些内容可能会经常改变,比如:人的年龄、身高、成绩分数、数学函数的计算结果等,对于这些经常改变的内容,在Java程序中,称为变量。而数据类型就是用来定义不同种类变量的。
  • 语法格式
数据类型 变量名 = 初始值;

在Java中,所有类型定义的变量都是有符号的.

举例:

double d = 3.14;
char c = 'A';
boolean b = true;
 
System.Out.println(a);
System.Out.println(d);
System.Out.println(c);
System.Out.println(b);
 
a = 100;  // a是变量,a中的值是可以修改的,注意:= 在java中表示赋值,即将100交给a,a中保存的值就是100
System.Out.println(a);
 
// 注意:在一行可以定义多个相同类型的变量
int a1 = 10, a2 = 20, a3 = 30;
System.Out.println(a1);
System.Out.println(a2);
System.Out.println(a3);

[注意] Java中不允许使用未初始化的局部变量.

3.3.2 整形变量

// 方式一:在定义时给出初始值
int a = 10;
System.Out.println(a);
 
// 方式二:在定义时没有给初始值,但使用前必须设置初值
int b;
b = 10;
System.Out.println(b);
 
// 使用方式二定义后,在使用前如果没有赋值,则编译期间会报错
int c;
System.Out.println(c);
c = 100;
 
// int型变量所能表示的范围:
System.Out.println(Integer.MIN_VALUE);
System.Out.println(Integer.MAX_VALUE);

[注意事项]

  1. int不论在何种系统下都是4个字节
  2. 推荐使用方式一定义,如果没有合适的初始值,可以设置为0
  3. 在给变量设置初始值时,值不能超过int的表示范围,否则会导致溢出
  4. 变量在使用之前必须要赋初值,否则编译报错.
  5. int的包装类型为 Integer,功能上比int更加强大,后续介绍.

3.3.3 长整形变量

int a = 10;
long b = 10;   // long定义的长整型变量
 
long c = 10L; // 为了区分int和long类型,一般建议:long类型变量的初始值之后加L或者l
long d = 10l; // 一般更加以加大写L,因为小写l与1不好区分
 
// long型变量所能表示的范围:这个数据范围远超过 int 的表示范围. 足够绝大部分的工程场景使用. 
System.Out.println(Long.MIN_VALUE);
System.Out.println(Long.MAX_VALUE);

[注意事项]

  1. 长整型变量的初始值后加L或者l,推荐加L.
  2. 长整型不论在那个系统下都占8个字节
  3. long的包装类型为Long(首字母大写)

3.3.4 短整型变量

short a = 10;
System.Out.println(a);
 
// short型变量所能表示的范围:
System.Out.println(Short.MIN_VALUE);
System.Out.println(Short.MAX_VALUE);

[注意事项]

  1. short在任何系统下都占2个字节
  2. short的表示范围为:-32768 ~ 32767
  3. 使用时注意不要超过范围(一般使用比较少)
  4. short的包装类型为Short(首字母大写)

3.3.5 字节类型变量

byte b = 10;
System.Out.println(b);
 
// byte型变量所能表示的范围:
System.Out.println(Byte.MIN_VALUE);
System.Out.println(Byte.MAX_VALUE);

[注意事项]

  1. byte在任何系统下都占1个字节
  2. byte的范围是:-128 ~ 127(这组数字需要记住,是一个常识)
  3. 字节的包装类型为Byte

3.3.6 浮点类型变量

3.3.6.1 双精度浮点数
double d = 3.14;
System.Out.println(d);

下面有两组代码:

int a = 1;
int b = 2;
System.out.println(a / b);
  • 这里我们在运行之后发现,输出的结果并不是0.5.这就是其中的一个注意事项:在 Java 中, int 除以 int 的值仍然是 int(会直接舍弃小数部分)。
double num = 1.1;
System.out.println(num * num);  
  • 这里我们发现输出的结果并不是1.21.只要是小数运算出的数字,都没有精确的结果.

[注意事项]

  1. double在任何系统下都占8个字节
  2. 浮点数与整数在内存中的存储方式不同,不能单纯使用2n的形式来计算
  3. double的包装类型为Double
  4. double 类型的内存布局遵守 IEEE 754 标准(和C语言一样), 尝试使用有限的内存空间表示可能无限的小数, 势必会存在一定的精度误差,因此浮点数是个近似值,并不是精确值
3.3.6.2 单精度浮点数
float num = 1.0f;    // 写作 1.0F 也可以,注意加上后缀
System.out.println(num);

float 类型在 Java 中占四个字节, 同样遵守 IEEE 754 标准. 由于表示的数据精度范围较小, 一般在工程上用到浮点数都优先考虑 double, 不太推荐使用 float.

3.3.7 字符类型变量

注意:与出语言不同,Java中的字符类型是2字节.

char c1 = 'A';    // 大写字母
char c2 = '1';    // 数字字符
 
System.out.println(c1);
System.out.println(c2);
 
// 注意:java中的字符可以存放整形
char c3 = '帅';
System.out.println(c3);

[注意事项]

  1. Java 中使用 单引号 + 单个字母 的形式表示字符字面值.
  2. 计算机中的字符本质上是一个整数. 在 C 语言中使用 ASCII 表示字符, 而 Java 中使用 Unicode 表示字符. 因此一个字符占用两个字节, 表示的字符种类更多, 包括中文.
  3. 关于汉字的编码方式,一般有两种,一种是Unicode方式,就是2字节,另一种是utf-8的方式,是3字节.
  4. char的包装类型为Character

3.3.8 布尔类型变量

布尔类型常用来表示真假.

boolean b = true;
System.out.println(b);
 
b = false;
System.out.println(b);

[注意事项]

  1. boolean 类型的变量只有两种取值, true 表示真, false 表示假.
  2. Java 的 boolean 类型和 int 不能相互转换, 不存在 1 表示 true, 0 表示 false 这样的用法.没有类似与出语言中,非0表示真,0表示假这这种说法.
  3. boolean的包装类型为Boolean.
  4. boolean并没有明确规定占几个字节,但是在Oracle公司的虚拟机实现中,boolean占1个字节.

3.3.9 类型转换

Java中作为一个强类型的编程语言,当不同类型之间的变量相互赋值的时候,会有较为严格的校验.

int a = 10;
long b = 100L;
b = a;   // 可以通过编译
a = b;   // 编译失败,不可以把表示范围较大的赋值给表示范围较小的.

在Java中,当参与运算的数据类型不一致的时候,就会进行类型转换,Java中类型转化主要分为3种,自动类型转换和强制类型转换.

3.3.9.1 自动类型转换

自动类型转换即:代码不需要经过任何处理,在代码编译时,编译器会自动进行处理。特点:数据范围小的转为数据范围大的时会自动进行
比如上面的那个例子.

3.3.9.2 强制类型转换

这种转换方式有一定的缺点:会发生数据截断,会发生数据丢失.
当进行操作时,代码需要经过一定的格式处理,不能自动完成。特点:数据范围大的到数据范围小
.在数据的前面加上用小括号加上括起来的数据类型来完成强制类型转换.

int a = 10;
long b = 100L;
b = a;        // int-->long,数据范围由小到大,隐式转换
a = (int)b;   // long-->int, 数据范围由大到小,需要强转,否则编译失败
 
float f = 3.14F;
double d = 5.12;
d = f;        // float-->double,数据范围由小到大,隐式转换
f = (float)d; // double-->float, 数据范围由大到小,需要强转,否则编译失败
 
a = d;   // 报错,类型不兼容
a = (int)d;   // int没有double表示的数据范围大,需要强转,小数点之后全部丢弃
 
byte b1 = 100;        // 100默认为int,没有超过byte范围,隐式转换
byte b2 = (byte)257;  // 257默认为int,超过byte范围,需要显示转换,否则报错
 
boolean flag = true;
a = flag;   // 编译失败:类型不兼容
flag = a;   // 编译失败:类型不兼容

[注意事项]

  1. 不同数字类型的变量之间赋值, 表示范围更小的类型能隐式转换成范围较大的类型.
  2. 如果需要把范围大的类型赋值给范围小的, 需要强制类型转换, 但是可能精度丢失.
  3. 将一个字面值常量进行赋值的时候, Java 会自动针对数字范围进行检查
  4. 强制类型转换不一定能成功,不相干的类型不能互相转换.比如我们前面提到的boolean类型和int类型.

3.3.10 整形提升

不同类型的数据之间相互运算时,数据类型小的会被提升到数据类型大的

  1. int与long之间:int会被提升为long
int a = 10;
long b = 20;
int c = a + b;  // 编译出错: a + b==>int + long--> long + long,出现编译出错的原因是,赋值给int会造成数据丢失.
long d = a + b;  // 编译成功:a + b==>int + long--->long + long 赋值给long  
  1. byte与byte的运算
byte a = 10;
byte b = 20;
byte c = a + b;
System.out.println(c);

上述代码会出现编译报错.我们由此可以得出以下结论:
byte 和 byte 都是相同类型, 但是出现编译报错. 原因是, 虽然 a 和 b 都是 byte, 但是计算 a + b 会先将 a和 b 都提升成 int, 再进行计算, 得到的结果也是 int, 这是赋给 c, 就会出现上述错误.
由于计算机的 CPU 通常是按照 4 个字节为单位从内存中读写数据. 为了硬件上实现方便, 诸如 byte 和 short这种低于 4 个字节的类型, 会先提升成 int, 再参与计算.
[小结]

  1. 对于不同类型的数据混合运算的时候,范围小的会提升为范围大的.
  2. 对于short和byte这种比4个字节小的类型,会先提升为4字节的int类型之后再进行运算.

3.3.11 字符串类型

String类型注意首字母大写,而且没有包装类.
在Java使用String类定义字符串类型,比如:

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = " world";
    System.out.println(s1);
    System.out.println(s2);
    System.out.println(s1+s2);   // s1+s2表示:将s1和s2进行拼接
}
  • Java中的字符串,没有所谓一’\0’结尾这一说.
  • Java中的字符串和其他数据类型进行+运算的时候,就是在进行字符串的拼接.
    比如下面还有一段容易出错的代码:
public class main {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println("hello"+a+b);
    }
}

运行结果是:
在这里插入图片描述
如果想要出现30 的效果,就要在a+b上加括号

public class main {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println("hello"+(a+b));
    }
}

在这里插入图片描述
关于字符串类型的一些其他操作,我们在后面介绍到String类型时候再进行说明.

4. 运算符

4.1 算数运算符

  1. 基本四则运算符:加减乘除模.(+ - * / %)
int a = 20;
int b = 10;
 
System.out.println(a + b);     // 30
System.out.println(a - b);     // 10
System.out.println(a * b);     // 200
System.out.println(a / b);     // 2
System.out.println(a % b);     // 0 --->模运算相当于数学中除法的余数

[注意]

  • 都是二元运算符,使用时必须要有左右两个运算符.
  • int/int结果还是int,而且会向下取整.
  • 做除法和取模的时候,做操作数不可以为0.
  • %不仅可以对整形取模,也可以对double类型取模,但是没有意义,一般都是对整数取模.
  • 两侧操作数类型不一致的时候,向范围大的类型提升.
  1. 增量运算符(+= -= *= %=)
    该种类型运算符操作完成后,会将操纵的结果赋值给左操作数。
int a = 1;
a += 2;                  // 相当于 a = a + 2
System.out.println(a);   // 输出3
a -= 1;                  // 相当于 a = a - 1
System.out.println(a);   // 输出2
a *= 3;                  // 相当于 a = a * 3
System.out.println(a);   // 输出6
a /= 3;                  // 相当于 a = a / 3
System.out.println(a);   // 输出2
a %= 3;                  // 相当于 a = a % 2
System.out.println(a);   // 输出2

[注意事项]
1. 只有变量可以进行增量运算,常量不可以.
2. 在进行增量运算的时候,会发生数据截断.和前面整形提升的原理正好相反.

  1. 自增/自减运算符符(++ --)
    ++是给变量的值+1,–是变量的值-1.
int a = 1;
a++;    // 后置++   表示给a的值加1,此时a的值为2
System.out.println(a++);   // 注意:后置++是先使用变量原来值,表示式结束时给变量+1,因此输出2
System.out.println(a);     // 输出3
++a;    // 前置++   表示给a的值加1
System.out.println(++a);   // 注意:前置++是先给变量+1,然后使用变量中的值,因此输出5
System.out.println(a);     // 输出5
// --操作符给操作-1,与++含义类似

[注意]

  • 如果单独使用,【前置++】和【后置++】没有任何区别
  • 如果混合使用,【前置++】先+1,然后使用变量+1之后的值,【后置++】先使用变量原来的值,表达式结束时给变量+1.
  • 只有变量才能使用自增/自减运算符,常量不能使用,因为常量不允许被修改.

4.2 关系运算符

关系运算符主要有六个: == != < > <= >= ,其计算结果是 true 或者 false.

int a = 10;
int b = 20;
// 注意:在Java中 = 表示赋值,要与数学中的含义区分
//     在Java中 == 表示相等
System.out.println(a == b);       // false
System.out.println(a != b);       // true
System.out.println(a < b);        // true
System.out.println(a > b);        // false
System.out.println(a <= b);       // true
System.out.println(a >= b);       // false

[注意事项]
当需要多次判断时,不能连着写,比如:3 < a < 5,Java程序与数学中是有区别的,需要写为a>3 && a<5.

4.3 逻辑运算符

逻辑运算符主要有三个: && || ! ,运算结果都是 boolean类型.

  1. 逻辑与&&
    语法规则:表达式1 && 表达式2,左右表达式必须是boolean类型的结果.
    在这里插入图片描述
int a = 1;
int b = 2;
 
System.out.println(a == 1 && b == 2);   // 左为真 且 右为真 则结果为真
System.out.println(a == 1 && b > 100);  // 左为真 但 右为假 则结果为假
System.out.println(a > 100 && b == 2);  // 左为假 但 右为真 则结果为假
System.out.println(a > 100 && b > 100); // 左为假 且 右为假 则结果为假
  1. 逻辑或 ||
    语法规则:表达式1 || 表达式2,左右表达式必须是boolean类型的结果.
    在这里插入图片描述
int a = 1;
int b = 2;
 
System.out.println(a == 1 || b == 2);   // 左为真 且 右为真 则结果为真
System.out.println(a == 1 || b > 100);  // 左为真 但 右为假 则结果也为真
System.out.println(a > 100 || b == 2);  // 左为假 但 右为真 则结果也为真
System.out.println(a > 100 || b > 100); // 左为假 且 右为假 则结果为假

左右表达式至少有一个为真,则结果为真.

  1. 逻辑非 !
    语法规则:! 表达式
    真变假,假变真
    在这里插入图片描述
int a = 1;
System.out.println(!(a == 1));   // a == 1 为true,取个非就是false
System.out.println(!(a != 1));  // a != 1 为false,取个非就是true
  1. 短路求值
    && 和 || 遵守短路求值的规则
System.out.println(10 > 20 && 10 / 0 == 0);             // 打印 false
System.out.println(10 < 20 || 10 / 0 == 0);             // 打印 true

我们都知道, 计算 10 / 0 会导致程序抛出异常. 但是上面的代码却能正常运行, 说明 10 / 0 并没有真正被求值
[注意]

  • 对于 && , 如果左侧表达式值为 false, 则表达式结果一定是 false, 无需计算右侧表达式.
  • 对于 ||, 如果左侧表达式值为 true, 则表达式结果一定是 true, 无需计算右侧表达式.
  • & 和 | 如果表达式结果为 boolean 时, 也表示逻辑运算. 但与 && || 相比, 它们不支持短路求值.

4.4 位运算符

位运算符主要有四个: & | ~ ^ ,除 ~ 是一元运算符外,其余都是二元运算符。
位操作表示按二进制位运算. 计算机中都是使用二进制来表示数据的(01构成的序列), 按位运算就是在按照二进制位的每一位依次进行计算.

  1. 按位与 &: 如果两个二进制位都是 1, 则结果为 1, 否则结果为 0.
    举例:10和20
    在这里插入图片描述
  2. 按位或 |: 如果两个二进制位都是 0, 则结果为 0, 否则结果为 1.
    在这里插入图片描述
  3. 按位取反 ~: 如果该位为 0 则转为 1, 如果该位为 1 则转为 0
  4. 按位异或 ^: 如果两个数字的二进制位相同, 则结果为 0, 相异则结果为 1.
int a = 0x1;
int b = 0x2;
System.out.printf("%x\n", a ^ b);

[注意] 如果两个数相同,则异或的结果为0.

举例:单身狗原则
1 ^ 2 ^ 3 ^2 ^ 1 == 3

4.5 移位运算符

移位运算符有三个: << >> >>> ,都是二元运算符,且都是按照二进制比特位来运算的.

  1. 左移 <<: 最左侧位不要了, 最右侧补 0.
int a = 0x10;
System.out.printf("%x\n", a << 1);
// 运行结果(注意, 是按十六进制打印的)
//20

[注意] 左移的时候,会丢弃符号位,因此正数左移可能会变成负数.
2. 右移 >>: 最右侧位不要了, 最左侧补符号位(正数补0, 负数补1)

int a = 0x10;
System.out.printf("%x\n", a >> 1);
 
// 运行结果(注意, 是按十六进制打印的)
//8
    
int b = 0xffff0000;
System.out.printf("%x\n", b >> 1);
 
// 运行结果(注意, 是按十六进制打印的)
//ffff8000
  1. 无符号右移 >>>: 最右侧位不要了, 最左侧补 0.
int a = 0xffffff;
System.out.printf("%x\n", a >>> 1);
 
// 运行结果(注意, 是按十六进制打印的)
//7fffffff

[结论]

  1. 左移 1 位, 相当于原数字 * 2. 左移 N 位, 相当于原数字 * 2 的N次方.
  2. 右移 1 位, 相当于原数字 / 2. 右移 N 位, 相当于原数字 / 2 的N次方.
  3. 由于计算机计算移位效率高于计算乘除, 当某个代码正好乘除 2 的N次方的时候可以用移位运算代替.

4.6 条件运算符(三目运算符)

表达式1 ? 表达式2 : 表达式3
当 表达式1 的值为 true 时, 整个表达式的值为 表达式2 的值;
当 表达式1 的值为 false 时, 整个表达式的值为 表达式3 的值
.
[注意事项]

  1. 表达式2和表达式3的结果要是同类型的,除非能发生类型隐式类型转换.
  2. 表达式不能单独存在,其产生的结果必须要被使用.

5. 逻辑控制

5.1 顺序结构

顺序结构比较简单,按照代码书写一行一行地执行就可以.

System.out.println("aaa");
System.out.println("bbb");
System.out.println("ccc");
 
// 运行结果
//aaa
//bbb
//ccc

5.2 分支结构

5.2.1 if语句

  1. 语法格式1
if(布尔表达式){
    // 语句
}

如果布尔表达式结果为true,执行if中的语句,否则不执行.

  1. 语法格式2
if(布尔表达式){
    // 语句1
}else{
    // 语句2
}

如果布尔表达式结果为true,则执行if中语句,否则执行else中语句.

  1. 语法格式
if(布尔表达式1){
    // 语句1
}else if(布尔表达式2){
    // 语句2
}else{
    // 语句3
}

表达式1成立,执行语句1,否则表达式2成立,执行语句2,否则执行语句3.
[建议] if/else语句下只有一条语句的时候,可以不加花括号,此时的else和最接近的if匹配.但是实际开发中,我们不建议这样写,最好加上大括号.

int x = 10;
int y = 10;
if (x == 10) 
	if (y == 10)
	System.out.println("aaa");
else 
 System.out.println("bbb");//虽然和第3行的if对齐,但是是和第4行的if匹配.

5.2.2 switch语句

  • 基本语法:
switch(表达式){
 case 常量值1:{
	 语句1;
	 [break;]
 }
 case 常量值2:{
	 语句2;
	 [break;]
 }
 ...
	 default:{
	 内容都不满足时执行语句;
	 [break;]
 } 
}
  • 执行流程
  1. 先计算表达式的值
  2. 和case依次比较,一旦有响应的匹配就执行该项下的语句,直到遇到break时结束
  3. 当表达式的值没有与所列项匹配时,执行default.
  • 注意事项
    • 多个case后的常量值不可以重复
    • switch的括号内不能是以下类型的数据:float,double,boolean,long.
    • break不要遗漏,否则就会失去多分支选择的效果.

5.3 循环结构

5.3.1 while循环

  • 语法格式
while(循环条件){
	循环语句;
}

循环条件为 true, 则执行循环语句; 否则结束循环.
[建议]

  • 和if类似,while下面可以不写花括号,但是不写的时候只能支持一条语句,建议还是加上花括号.
  • 和if类似,while后面的{建议和while写在同一行.

5.3.2 break

break 的功能是让循环提前结束.
示例:找到 100 - 200 中第一个 3 的倍数

int num = 100;
while (num <= 200) {
    if (num % 3 == 0) {
        System.out.println("找到了 3 的倍数, 为:" + num);
        break;
   }
    num++;
}

5.3.3 continue

continue 的功能是跳过这次循环, 立即进入下次循环.
示例:找到 100 - 200 中所有 3 的倍数

int num = 100;
while (num <= 200) {
    if (num % 3 != 0) {
        num++; 
        continue;
   }
    System.out.println("找到了 3 的倍数, 为:" + num);
    num++;
}

执行到 continue 语句的时候, 就会立刻进入下次循环(判定循环条件), 从而不会执行到下方的打印语句.

5.3.4 for循环

  • 基本语法
for(表达式①;布尔表达式②;表达式③){
	表达式④;
}
  • 表达式1: 用于初始化循环变量初始值设置,在循环最开始时执行,且只执行一次
  • 表达式2: 循环条件,满则循环继续,否则循环结束
  • 表达式3: 循环变量更新方式
    [注意事项] 和while类似,不再赘述

5.4 输入与输出

5.4.1 输出到控制台

  • 基本语法
System.out.println(msg);            // 输出一个字符串, 带换行
System.out.print(msg);              // 输出一个字符串, 不带换行
System.out.printf(format, msg); // 格式化输出
  1. println 输出的内容自带 \n, print 不带 \n
  2. printf 的格式化输出方式和C 语言的 printf 是基本一致的.

示例:

System.out.println("hello world");
 
int x = 10;
System.out.printf("x = %d\n", x)

5.4.2 从键盘输入

使用 Scanner 读取字符串/整数/浮点数.
举例说明:

import java.util.Scanner;  // 需要导入 util 包
 
Scanner sc = new Scanner(System.in);
System.out.println("请输入你的姓名:");
String name = sc.nextLine();
System.out.println("请输入你的年龄:");
int age = sc.nextInt();
System.out.println("请输入你的工资:");
float salary = sc.nextFloat();
System.out.println("你的信息如下:");
System.out.println("姓名: "+name+"\n"+"年龄:"+age+"\n"+"工资:"+salary);
sc.close(); // 注意, 要记得调用关闭方法

解释:

  • 创建一个Scanner类型的对象,使用sc引用.
  • 之后调用sc中的next()系列的方法.
    • next():返回字符串,自动消除有效字符之前的空格,直到结束符.(换行)
    • nextLine():返回字符串,不会自动消除空格,录入所有的字符,直到回车.
    • nextInt():返回十进制整数.
    • nextFloat():返回浮点数.
  • sc实质上是一个流对象的引用,我们在最后需要关闭.

6. 方法的使用

6.1 方法的概念与使用

6.1.1 什么是方法

方法就是一个代码片段. 类似于 C 语言中的 “函数”.

  1. 是能够模块化的组织代码(当代码规模比较复杂的时候).
  2. 做到代码被重复使用, 一份代码可以在多个位置使用.
  3. 让代码更好理解更简单.
  4. 直接调用现有方法开发, 不必重复造轮子.

6.1.2 方法的定义

  • 语法格式
修饰符 返回值类型 方法名称([参数类型 形参 ...]){
	方法体代码;
	[return 返回值];
}

示例:判断一个年份是否是闰年

public class Method{
	    // 方法定义
    public static boolean isLeapYear(int year){
		if((0 == year % 4 && 0 != year % 100) || 0 == year % 400){
		   return true;
		 }else{
		   return false;
		 }
	 }
}

[注意事项]

  1. 修饰符:现阶段直接使用public static 固定搭配
  2. 返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成void,注意这里的返回值 和c语言有所不同,这里返回的参数还可以是数组类型.
  3. 方法名字:采用小驼峰命名
  4. 参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开
  5. 方法体:方法内部要执行的语句
  6. 在java当中,方法必须写在类当中
  7. 在java当中,方法不能嵌套定义.

6.1.3 方法调用的执行过程

调用方法—>传递参数—>找到方法地址—>执行被调方法的方法体—>被调方法结束返回—>回到主调方法继续往下执行

  • 方法参数传递的注意事项
  1. 匹配参数的个数
  2. 匹配参数的类型
  3. 匹配参数的顺序
  4. 匹配返回值的类型

6.1.4 实参和形参的关系

方法的形参相当于数学函数中的自变量,比如:1 + 2 + 3 + … + n的公式为sum(n) = (1+n)*n/2.
Java中方法的形参就相当于sum函数中的自变量n,用来接收sum函数在调用时传递的值的。形参的名字可以随意取,对方法都没有任何影响,形参只是方法在定义时需要借助的一个变量,用来保存方法在调用时传递过来的值.

public static int getSum(int N){    // N是形参
    return (1+N)*N / 2;
}
getSum(10);      // 10是实参,在方法调用时,形参N用来保存10
getSum(100);     // 100是实参,在方法调用时,形参N用来保存100

[注意] 在Java中,实参的值永远都是临时拷贝到形参中,形参和实参本质是两个实体.
代码实例:交换两个整形变量.

public class TestMethod {
	public static void main(String[] args) {
		int a = 10;
		int b = 20;
		swap(a, b);
		System.out.println("main: a = " + a + " b = " + b);
	}
	
	public static void swap(int x, int y) {
		int tmp = x;
		x = y;
		y = tmp;
		System.out.println("swap: x = " + x + " y = " + y);
	}
}
 
// 运行结果
//swap: x = 20 y = 10
//main: a = 10 b = 20

可以看到,在swap函数交换之后,形参x和y的值发生了改变,但是main方法中a和b还是交换之前的值,即没有交换成功.

  • 原因解释:
    实参a和b是main方法中的两个变量,其空间在main方法的栈(一块特殊的内存空间)中,而形参x和y是swap方法中的两个变量,x和y的空间在swap方法运行时的栈中,因此:实参a和b 与 形参x和y是两个没有任何关联性的变量,在swap方法调用时,只是将实参a和b中的值拷贝了一份传递给了形参x和y,因此对形参x和y操作不会对实参a和b产生任何影响.
    在这里插入图片描述
    [总结] 对于基础类型来说,形参相当于实参的拷贝.即传值调用.
  • 解决办法
    使用引用类型参数.(比如数组)
public class TestMethod {
	public static void main(String[] args) {
	int[] array = {10, 20};
	swap(array);
	System.out.println("array[0] = " + array[0] + " array[1] = " + array[1]);
	}
	
	public static void swap(int[] arr) {
	int tmp = arr[0];
	arr[0] = arr[1];
	arr[1] = tmp;
	}
}
 
// 运行结果
//arr[0] = 20 arr[1] = 10

在这里插入图片描述

6.2 方法的重载

6.2.1 方法重载的概念

在Java中,如果多个方法的名字相同,参数列表不同,则称该几种方法被重载了.
参数列表不同一般指的是:个数,数据类型,或者是顺序不同.
返回值的类型是否一样,不影响方法的重载.即只有方法的名字是一样的,参数列表必须修改,返回值类型可以修改.
举例:下面这几个方法就相互构成重载.

public class TestMethod {
	public static void main(String[] args) {
	add(1, 2);                // 调用add(int, int)
	add(1.5, 2.5);            // 调用add(double, double)
	add(1.5, 2.5, 3.5);       // 调用add(double, double, double)
	}
 
	public static int add(int x, int y) {
	return x + y;
	}
 
	public static double add(double x, double y) {
	return x + y;
	}
	
	public static double add(double x, double y, double z) {
	return x + y + z;
	}
}

6.2.2 方法签名

在同一个作用域中不能定义两个相同名称的标识符。比如:方法中不能定义两个名字一样的变量,那为什么类中就可以定义方法名相同的方法呢?
方法签名即:经过编译器编译修改过之后方法最终的名字。具体方式:方法全路径名+参数列表+返回值类型,构成方法完整的名字

6.3 递归

6.3.1 递归的概念

从前有坐山,山上有座庙,庙里有个老和尚给小和尚将故事,讲的就是:
"从前有座山,山上有座庙,庙里有个老和尚给小和尚讲故事,讲的就是:
“从前有座山,山上有座庙…”
“从前有座山……”
在这里插入图片描述
上面这个故事有一个特征:自身中又包含了自己.我们从而引出递归的概念.
一个方法在执行过程中调用自身, 就称为 “递归”.
递归的必要条件:

  • 将原问题划分为其子问题.但子问题和原问题的解法相同.
  • 递归结束条件

示例:递归求n的阶乘

public static void main(String[] args) {
    int n = 5;
    int ret = factor(n);
    System.out.println("ret = " + ret);
}
 
public static int factor(int n) {
    if (n == 1) {
        return 1;
   }
    return n * factor(n - 1); // factor 调用函数自身
}
 
// 执行结果
//ret = 120

6.3.2 递归执行过程分析

我们还是那上面n的阶乘来说明:

public static void main(String[] args) {
    int n = 5;
    int ret = factor(n);
    System.out.println("ret = " + ret);
}
 
public static int factor(int n) {
 System.out.println("函数开始, n = " + n);
    if (n == 1) {
        System.out.println("函数结束, n = 1 ret = 1");
        return 1;
   }
    int ret = n * factor(n - 1);
    System.out.println("函数结束, n = " + n + " ret = " + ret);
    return ret;
}
 
// 执行结果
//函数开始, n = 5
//函数开始, n = 4
//函数开始, n = 3
//函数开始, n = 2
//函数开始, n = 1
//函数结束, n = 1 ret = 1
//函数结束, n = 2 ret = 2
//函数结束, n = 3 ret = 6
//函数结束, n = 4 ret = 24
//函数结束, n = 5 ret = 120
//ret = 120

在这里插入图片描述

7. 类与对象

7.1 如何创建类与对象

  1. 什么是面向对象
    在面向对象的世界里,一切皆为对象,面向对象是一种解决问题的思想,主要依靠对象之间的交互完成一件事情。除了Java,面相对象的程序设计还有C++,Java语言和C++语言是当今互联网企业后端开发最主流的两门语言。

  2. 面向过程与面相对象
    拿一个生活中的常见例子来说明,比如洗衣服,我们既可以用手洗,也可以用洗衣机洗。手洗分为很多过程,少了一个环节都不行,所以手洗比较麻烦,代码也是同样的道理,这种面相过程来程序设计,会特别麻烦,特别是在扩展和维护阶段。
    但是如今我们有了洗衣机,我们就只需要把衣服放入洗衣机里,在你的面前只有衣服,人和洗衣机,这时候你不必关心洗衣服的过程,而且洗衣服的时间可以自由调节,代码也是一样,面向对象的程序设计会让你不必担心执行的过程,就像你把衣服扔进洗衣机什么都不用管一样,而且可维护性也是比较好的,就像你可以自由调节洗衣机的洗衣时间一样方便。
    ​​​​​​
    ​​在这里插入图片描述

  3. 类的定义和使用
    前面讲到,面相对象的程序设计关心的是现实生活中的实体,但是计算机并不知道任何现实生活中的东西,所以我们就要来用一种语法来告诉计算机这个类型的东西是个啥,长啥样,有啥用。
    比如我们就拿我们可爱的小狗狗来举例子
    首先我们引入类的定义格式:

class ClassName{
	field//成员变量
	method//成员方法
}

我们这里提出一点小建议,我们命名类名的时候尽量采用大驼峰形式,即每个单词的首字符大写
class为定义类的关键字.

接下来我们来定义一个狗类

class Dog{
    public String name;//名字
    public int age;//年龄
//狗的行为
    public void barks(){
        System.out.println(name+"汪汪汪");
    }
    public void wag(){
        System.out.println(name+"摇尾巴");
    }
}

3.类的实例化

我们在计算机里定义了一个类,就相当于我们在计算机里有了一个新的数据类型,就和int double这种一样,而新的数据类型我们要创建一个新的变量来使用它,这个过程就叫类的实例化,也叫创建对象的过程,在Java中,我们用new关键字来实例化对象,语法格式如下

ClassName1 objectname = new ClassName1(...);

我们接着上面的的狗类来举例子

public class x {
    public static void main(String[] args) {
        Dog dog1=new Dog();
        dog1.name="哈士奇";
        dog1.age=3;
        dog1.barks();
        dog1.wag();
    }
}
class Dog{
    public String name;//名字
    public int age;//年龄
    public void barks(){
        System.out.println(name+"汪汪汪");
    }
    public void wag(){
        System.out.println(name+"摇尾巴");
    }
}

从上述的实例化我们不难看出,其实类就是一个抽象的事物,我们需要通过实例化来对这个抽象的事物进行一个具体的描述。就比如狗,每一只狗同有这些行为和属性,但是不同家里的狗表现出的这些属性和行为是不一样的,它们是两个不一样的对象,但是它们都属于狗类。

7.2 this引用

7.2.1 为什么要用this引用

class Date {
    public int year;
    public int month;
    public int day;
    public void setday(int y,int m,int d){
        year=y;
        month=m;
        day=d;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date = new Date();
        date.setday(2023,12,4);
        date.printday();
    }
}

这里我们定义了一个日期类,并且创建了一个他的对象,但是如果出现了一下的问题

public void setday(int year,int month,int day){
        year=year;
        month=month;
        day=day;
    }

这时候这里的year,month,day,你怕是傻傻分不清了吧,哪个是形式参数?哪个是成员变量?这时候,我们就要引入this关键字了。

7.2.2 何为this引用

this引用指向的是当前对象(成员方法运行时候调用该成员方法的对象),在成员方法所有成员变量的操作,都是通过该引用去访问。常见的使用方法有以下几种:

  1. this.成员变量——>调用当前对象的成员变量
  2. this.成员方法——>调用当前对象的成员方法
  3. this——>当前对象
  4. this()------>当前对象的构造方法
    那么,下面我们就使用this引用对上述代码进行修改
class Date {
    public int year;
    public int month;
    public int day;
    public void setday(int year,int month,int day){
        this.year=year;
        this.month=month;
        this.day=day;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date = new Date();
        date.setday(2023,12,4);
        date.printday();
    }
}

这里的this引用的是date变量中,也就是Date类一个实例化对象的成员变量,这里就可以区分开成员变量和形式参数了。

7.2.3 this引用的特性

  1. this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型
  2. this只能在"成员方法或构造方法中使用(后续介绍)"中使用
  3. 在"成员方法"中,this只能引用当前对象,不能再引用其他对象
  4. this是“成员方法”第一个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器会负责将调用成员方法对象的引用传递给该成员方法,this负责来接收

7.3 对象的构造及其初始化

7.3.1 如何初始化对象

通过前面的知识,我们知道,在Java中定义局部变量时候,必须要初始化,否者编译器就会报错。如果是对象的话,

public class Date {
    public int year;
    public int month;
    public int day;
    public void setday(int year,int month,int day){
        this.year=year;
        this.month=month;
        this.day=day;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date = new Date();
        date.setday(2023,12,4);
        date.printday();
    }
}

就如前面类与对象中第二篇展示的一样,我们必须要通过调用一个setday方法来初始化对象的具体日期,从这段代码中,我们可以发现两个问题:

  1. 我们需要调用成员方法来对日期进行初始化,有什么方法可以简化一下吗?
  2. 局部变量在初始化才可以使用,但是为什么这里的成员变量声明之后没有给出初始值依然可以使用?

7.3.2 构造方法

7.3.2.1调用构造方法的快捷键

在idea 64编译器中,alt+insert键会调用出构造方法列表。

7.3.2.2概念

构造方法是一个特殊的成员方法,名字必须与类名相同,在创建对象的时候,由编译器自动调用,即在创建对象的时候,编译器会干两件事情:

  1. 为创建的对象分配内存空间
  2. 由编译器自动调用该对象的额构造方法
7.3.2.3特性
  1. 名字必须与类名相同
  2. 没有返回值类型
  3. 构造方法可以重载,程序员可根据自己的需要提供参数不同的构造方法
public class Date {
    public int year;
    public int month;
    public int day;
   public Date(){//无参构造方法
        this.year=2023;
        this.month=12;
        this.day=5;
    }
   public Date(int year,int month,int day){//有参构造方法
        this.year=year;
        this.month=month;
        this.day=day;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date1 = new Date();//调用无参
        date1.printday();
        Date date2 = new Date(2023,12,5);//调用有参
        date2.printday();
    }
}

上述两个方法便构成了重载

  1. 如果没有显式定义,编译器就会自动生成一个默认的不带任何参数的构造方法
public class Date {
    public int year;
    public int month;
    public int day;
    /*Date(){
    }这里会有一个默认的构造方法*/
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date = new Date();
        date.printday();
    }
}
  1. 如果用户有自己定义构造方法,那么编译器便不再提供默认的构造方法(救急不救穷
public class Date {
    public int year;
    public int month;
    public int day;
   public Date(int year,int month,int day){
        this.year=year;
        this.month=month;
        this.day=day;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date1 = new Date();//错误,编译器找不到这样的一个构造方法
        date2.printday();
    }
}
  1. 但是我们可以通过this方法来修改上述的错误,这里的this如果后面没有任何的成员变量的话,直接跟上的是一个参数列表,就表示的是该类的构造方法.
public class Date {
    public int year;
    public int month;
    public int day;
   public Date(){
    this(2023,12,5);
    }//this调用对应的构造方法
   public Date(int year,int month,int day){
        this.year=year;
        this.month=month;
        this.day=day;
    }
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date1 = new Date();//错误,编译器找不到这样的一个构造方法
        date2.printday();
    }
}

这里需要注意的一点是this必须是构造方法中的第一条语句,否者编译失败
7. 大多数情况下都是用public修饰,有时候也会用private修饰,后面细说

7.3.3 默认初始化

在第一个大标题中,我们提出了两个问题,但是我们的第二个问题还没有提及,下面我们直接给出回答:成员变量在不初始化的时候,会有默认的初始值,在对象的空间被申请之后,如果没有就地初始化,对象中的成员变量就已经有了默认的初始值,至于初始值,注意两种类型就可以

数据类型默认值
booleanfalse
reference(引用类型)null

7.3.4 就地初始化

在声明成员变量的时候,直接给出初始值

public class Date {
    public int year=2023;
    public int month=12;
    public int day=5;
    public void printday(){
        System.out.println(year+"/"+month+"/"+day);
    }
    public static void main(String[] args) {
        Date date = new Date();
        date.printday();
    }
}

7.4 封装

7.4.1 封装的概念

面向对象的程序设计有三个特点:封装继承和多态。那么什么是封装呢,通俗一点讲就是:套壳屏蔽细节,使得类更加安全。
就像我们在市场上买了一台电脑,电脑的内部是主板以及CPU等硬件,但是向用户交付产品的时候,我们总不能向用户交付一个电路板,我们需要对这些电路板进行包装,做出一台电脑应该有的样子,并在电脑的旁边提供一些接口,方便用户和电脑内部的硬件进行一系列的交流,这就对一些硬件进行了封装。
在这里插入图片描述在这里插入图片描述
那么什么是封装呢?将数据和数据操作的方法进行有机结合,隐藏对象的属性和细节,仅对外公开接口来和对象进行交互

7.4.2 访问限定符

那么怎么实现封装呢,Java主要通过访问限定符来进行封装,访问权限用来控制方法或者字段是否可以直接在类外使用,下面是Java的四种访问限定符:

NO范围privatedefaultprotectedpublic
1同一包中的同一类
2同一包中的不同类×
3不同包中的子类××
4不同包中的非子类×××

注意:这里的default并不是关键字,指的是什么都不写的情况下,就是没有修饰限定符的时候

这里可以形象地理解:假如一个类是一个人

  • public:这个人的外貌特征,谁都可以看见
  • protected:在别人那里,只有别人家的孩子们知道,但是他们的家长不知道
  • default:只有自己家中的人知道,外人不知道
  • private:只有自己知道,其他人都不知道

eg:假如我们创建一个“计算机”对象

public class Computer {
    private String cpu;//中央处理器
    private String memory;//内存
    String screen;//屏幕
    public String brand;//品牌
     public Computer(String cpu,String memory,String screen,String brand){
         this.cpu=cpu;
         this.memory=memory;
         this.screen=screen;
         this.brand=brand;
     } 
     public void poweron(){
         System.out.println("开机");
     }
     public void poweroff(){
         System.out.println("关机");
     }
}
public class TestComputer {
    public static void main(String[] args) {
        Computer computer=new Computer("intel 7","32G","16.1","hp");
        System.out.println(computer.brand);
        System.out.println(computer.screen);
        //System.out.println(computer.cpu);错误,cpu为privat类型,不可在类外访问
    }
}

7.4.3 包

7.4.3.1 包的概念

我们为了更好地管理类,把多个类收集在一起成为一组,称为包,说的再直白一些,就是文件夹,有点类似于目录。只不过现在文件夹中装的是一个个的类。
在这里插入图片描述
包是对类、接口等的封装机制的体现,一种对类或者接口的很好的组织方式,这里要注意的是:在同一个工程中允许存在相同名称的类,只要处在不同的包中即可

7.4.3.2 导包

导包,即导入包中的类,关键字为import,Java中已经提供了很多现成的类供我们使用,如Date类。我们下面通过代码来演示,我们可以通过Date类得到一个时间戳。

import java.util.Date;//导包

public class Main {
    public static void main(String[] args) {
        Date date=new Date();
        System.out.println(date.getTime());
    }
}

如果不知道导入其中一个包中的哪个类,可以使用通配符“*”。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Date date=new Date();
        System.out.println(date.getTime());
    }
}

但是我们一般建议导包的时候要明确类名,避免出现冲突的情况

若包中有静态方法和字段,可使用import static来导入,例如:import static java.lang.Math.*

7.4.3.3 自定义包

1 基本规则

  • 在文件的最上方加上一个package语句来指定该代码在哪个包中
  • 包名需要尽量指定为唯一的名字
  • 包名和代码路径相匹配,例如创建com.by.demo1的包,对应的路径为com/by/demo1
  • 若一个类没有package语句,则会被放到idea默认的src包下

2 操作步骤

  1. 在idea中新建一个包,如图所示在这里插入图片描述
  2. 在弹窗中命名包名
    在这里插入图片描述
  3. 在包中创建类
    在这里插入图片描述
  4. 我们看到类已经被创建了,包含了package语句,路径也是对应的

在这里插入图片描述

在这里插入图片描述

7.4.3.4 基于包的访问权限

我们还是拿上面的计算机类来举例

package com.by.demo1
public class Computer {
    private String cpu;//中央处理器
    private String memory;//内存
    String screen;//屏幕
    public String brand;//品牌
     public Computer(String cpu,String memory,String screen,String brand){
         this.cpu=cpu;
         this.memory=memory;
         this.screen=screen;
         this.brand=brand;
     } 
     public void poweron(){
         System.out.println("开机");
     }
     public void poweroff(){
         System.out.println("关机");
     }
}
package com.by.demo2
public class TestComputer {
    public static void main(String[] args) {
        Computer computer=new Computer("intel 7","32G","16.1","hp");
        System.out.println(computer.brand);
        //System.out.println(computer.screen);错误,screen为默认类型,不可在包外的其他类访问
        //System.out.println(computer.cpu);错误,cpu为privat类型,不可在类外访问
    }
}
7.4.3.5常见的包
  • java.sql:做数据库会用到
  • java.util:工具程序包,如ArrayList,List类都在这个包中,后期学习数据结构会用到
  • java.net:网络开发会用到
  • java.lang:系统常用基础类,如String,Object,从jdk1.1之后就会自动导入

7.5 static成员

7.5.1 为什么要使用static成员

有一些通过类实例化出的对象,它们具有相同的特点,在实例化一个类时候,每次都实例化相同的属性或方法,显得有些麻烦,所以我们便引入了static成员。
我们都一样

static成员,又叫静态成员,也可以称作类成员,其不属于某个具体的对象,它是所有对象索共有的。下面我们拿“学生类”来举例。

public class Student {
    public String name;
    public int age;
    public String classroom;

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

    public void classbegin(){
        System.out.println("在同一间教室上课");
    }

    public static void main(String[] args) {
        Student s1=new Student("张三",20,"111");
        s1.classbegin();
        Student s2=new Student("李四",19,"111");
        s2.classbegin();
    }
}

从这段代码中,我们发现,张三和李四上课的教室是同一间教室,都是111班,这样每一次都对classroom进行初始化和每次调用classbegin方法显得过于麻烦,所以我们对以上代码使用类成员进行改进。

public class Student {
    public String name;
    public int age;
    public static String classroom="111";

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

    public static void classbegin(){
        System.out.println("在同一间教室上课");
    }

    public static void main(String[] args) {
        Student s1=new Student("张三",20);
        Student s2=new Student("李四",19);
        Student.classbegin();//这里先不用管,讲到后面会明白
    }
}

7.5.2 static修饰成员变量

static修饰的成员变量,称为静态成员变量
静态成员变量的特点:

  • 不属于某一个对象,是类的属性,是所有对象所共有的(最大的特点)
  • 既可以通过对象访问(会报警告),也可以通过类名访问,更推荐第二种。
  • 类变量储存在方法区中
  • 生命周期伴随类的一生(随着类的加载而创建,随着类的销毁而销毁

7.5.3 static修饰成员方法

我们对以上“学生类”代码进行一些简单修改

public class Student {
    public String name;
    public int age;
    private static String classroom = "111";

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

    public static String classbegin() {
        //System.out.println(this.name);  error
        return classroom;
    }
}
public class Test {
    public static void main(String[] args) {
        Student s1=new Student("张三",20);
        Student s2=new Student("李四",19);
        System.out.println(Student.classbegin());
    }
}

我们把类中的classroom进行了封装限定,并通过公开的classbegin接口来获取到classroom,并把主函数移动到了一个不同的类当中,在主函数中使用类名来访问classbegin方法。
从以上代码,我们可以看出static修饰成员方法的一些特点:

  • 不属于某个具体的对象,是类方法
  • 既可以通过对象访问(会报警告),也可以通过类名访问,更推荐第二种。
  • 不可以在静态方法中访问任何非静态成员变量
  • 同上面第三条的原理在静态方法中不可以调用任何非静态的方法
  • 但是反过来,非静态的成员方法可以调用静态成员变量或者静态成员方法,举例如下。
public class Student {
    public String name;
    public int age;
    private static String classroom = "111";
    public void test(){
		classbegin();//未报错
    }

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

    public static String classbegin() {
        return classroom;
        //this.test; error
    }
}
public class Test {
    public static void main(String[] args) {
        Student s1=new Student("张三",20);
        Student s2=new Student("李四",19);
        System.out.println(Student.classbegin());
    }
}
  • 静态方法无法重写,无法实现多态(后面会讲到)

7.5.4 static成员变量的初始化

注意:static成员变量不可以放在构造方法中来初始化因为构造方法是一个类创建实例化对象的时候自动调用的方法,而静态成员变量和对象这个概念压根不搭边,也就不可以在构造方法中调用.
他的初始化方法只有两种,就地初始化和静态代码块初始化

  1. 就地初始化是指:在定义的时候就给出初始值,就如上面的“学生类”
private static String classroom = "111";
  1. 静态代码块初始化
    后续会讲到

7.6 代码块

7.6.1 代码块的概念以及分类

使用{}定义的一段代码称为代码块,代码块可以分为以下四种:

  • 普通代码块
  • 构造代码块
  • 静态代码块
  • 同步代码块(后续多线程问题会谈到)

7.6.2 普通代码块

定义在方法中的代码块,这类代码块不需要条件,一定会被执行,但是这种代码块相对少见

public class Main{
public static void main(String[] args) {
	{ //直接使用{}定义,普通方法块
		int x = 10 ;
		System.out.println("x1 = " +x);
	}
	int x = 100 ;
	System.out.println("x2 = " +x);
}
}

7.6.3 构造代码块

定义在类中的代码块(不加修饰符)。也叫实例代码块构造代码块一般用于初始化实例成员对象。还是拿我们熟悉的“学生类”来举例。

public class Student{
//实例成员变量
	private String name;
	private String gender;
	private int age;
	private double score;
	public Student() {
		System.out.println("I am Student init()!");
	}
//实例代码块
	{
		this.name = "bit";
		this.age = 12;
		this.sex = "man";
		System.out.println("I am instance init()!");
	}
	public void show(){
		System.out.println("name: "+name+" age: "+age+" sex: "+sex);
	}
}
public class Main {
	public static void main(String[] args) {
		Student stu = new Student();
		stu.show();
	}
}

7.6.4 静态代码块

使用static修饰的代码块称为静态代码块,一般用于初始化静态成员变量

public class Student{
	private String name;
	private String sex;
	private int age;
	private double score;
	private static String classRoom;
	//实例代码块
	{
		this.name = "bit";
		this.age = 12;
		this.gender = "man";
		System.out.println("I am instance init()!");
	}
	// 静态代码块
	static {
		classRoom = "bit306";
		System.out.println("I am static init()!");
	}
	public Student(){
		System.out.println("I am Student init()!");
	}
	public static void main(String[] args) {
		Student s1 = new Student();
		Student s2 = new Student();
	}
}

这里可以返回去看一下上一篇文章中静态成员变量的初始化
注意事项

  • 静态代码块不管生成多少个对象,其只会执行一次
  • 静态成员变量是类的属性,就和我们上一篇文章提到的类成员性质一样
  • 实例代码块只有在创建对象的时候才会执行

7.6.5 代码块执行的顺序

静态代码块>实例代码块>构造方法
原理解释
在代码执行的时候,先要加载出类,才可以对类进行实例化,然而静态代码块属于类的属性,只要类被加载,就会执行不管有没有对这个类做出实例化,所以静态代码块>实例代码块,其次,创建一个对象的时候,分为两步,第一步是给创建的对象分配内存空间,其次调用对象的构造方法,所以实例代码块>构造方法

8. 继承与多态

8.1 继承

8.1.1 为什么需要继承

Java中使用类对现实中的事物进行描述的时候,由于世间事物错综复杂,事物之间难免会存在一些特定的关联,这就是程序设计时候所需要考虑的问题。
比如:猫和狗都是动物
在这里插入图片描述
在这里插入图片描述我们使用Java语言就会有如下描述

public class Dog{
	string name;
	int age;
	float weight;
	public void eat(){
		System.out.println(name + "正在吃饭");
	}
public void sleep(){
		System.out.println(name + "正在睡觉");
	}
void Bark(){
		System.out.println(name + "汪汪汪~~~");
	}
}
public class Cat{
	string name;
	int age;
	float weight;
	public void eat(){
		System.out.println(name + "正在吃饭");
	}
	public void sleep()
	{
		System.out.println(name + "正在睡觉");
	}
	void mew(){
		System.out.println(name + "喵喵喵~~~");
	}
}

在这里插入图片描述

通过上述代码发现,猫类和狗类中有大量重复的成员,它们都有自己的名字,年龄,体重,它们都会吃,会睡觉,只有叫声是不一样的。那么我们能否对这写共性进行抽取呢?面向对象的程序设计思想中提出了继承的概念,专门用来进行共性抽取,以实现代码的复用

8.1.2 继承的概念

继承的机制:这是面向对象程序设计中使代码可以复用的最重要的手段,他允许程序员在保持一个类原有特征的同时进行扩展增加功能,这样产生的类,称为派生类。很好地体现了面向对象程序设计的清晰的层次结构。总之,继承所解决的问题就是:共性的抽取,实现代码的复用
在这里插入图片描述
如上图所示,Dog和Cat类都继承了Animal类,其中Animal类称为父类,基类,或者超类,Dog和Cat类称为子类或者派生类,继承之后,子类就可以复用父类中的成员,子类在实现时只关心自己的成员即可。

8.1.3 继承的语法格式

在Java中入如果要表示继承关系,需要使用extends关键字,具体格式如下:

修饰符 class 子类 extends 父类{
//
}

现在,我们使用继承语法对猫类和狗类进行重新设计:

public class Animal{
	String name;
	int age;
	public void eat(){
		System.out.println(name + "正在吃饭");
	}
	public void sleep(){
		System.out.println(name + "正在睡觉");
	}
}
public class Dog extends Animal{
	void bark(){
		System.out.println(name + "汪汪汪~~~");
	}
}
public class Cat extends Animal{
	void mew(){
		System.out.println(name + "喵喵喵~~~");
	}
}

注意事项

  • 子类会将父类的成员变量成员方法继承到子类中
  • 子类继承父类之后,必须要新添加自己特有的成员,否则就没必要继承了
  • static修饰的成员不可以继承,因为他们不属于对象,他们属于类(这时候可以返回上一篇文章看看删掉的那一行了)

8.1.4 父类成员访问

在继承体系中,既然子类把父类的方法和字段都继承下来了,那子类怎样去访问父类中的成员呢?

8.1.4.1子类中访问父类的成员变量
  1. 子类和父类不存在同名的情况
public class Base {
	int a;
	int b;
}
public class Derived extends Base{
	int c;
	public void method(){
		a = 10; // 访问从父类中继承下来的a
		b = 20; // 访问从父类中继承下来的b
		c = 30; // 访问子类自己的c
	}
}

不同名的情况下,可直接访问父类成员

  1. 子类和父类同名的情况
public class Base {
	int a;
	int b;
	int c;
}
public class Derived extends Base{
	int a; // 与父类中成员a同名,且类型相同
	char b; // 与父类中成员b同名,但类型不同
	public void method(){
		a = 100; // 访问父类继承的a,还是子类自己新增的a?自己的
		b = 101; // 访问父类继承的b,还是子类自己新增的b? 自己的
		c = 102; // 子类没有c,访问的肯定是从父类继承下来的c
	}
}

子类中的成员访问遵循以下原则

  • 如果访问的成员变量中,子类里有,优先访问自己的成员变量
  • 如果访问的成员变量中,子类中没有,但父类中有,则访问父类继承下来的
  • 如果访问的成员变量子类和父类重名,优先访问自己的

成员变量的访问遵循就近访问原则,自己有优先用自己的,如果没有再从父类中去找

8.1.4.2 子类中访问父类的成员方法
  1. 成员方法不同名
public class Base {
	public void methodA(){
		System.out.println("Base中的methodA()");
	}
}
public class Derived extends Base{
	public void methodB(){
		System.out.println("Derived中的methodB()方法");
	}
	public void methodC(){
		methodB(); // 访问子类自己的methodB()
		methodA(); // 访问父类继承的methodA()
	}
}
  1. 成员方法同名
public class Base {
	public void methodA(){
		System.out.println("Base中的methodA()");
	}
	public void methodB(){
		System.out.println("Base中的methodB()");
	}
}
public class Derived extends Base{
	public void methodA(int a) {
		System.out.println("Derived中的method(int)方法");//重载
	}
	public void methodB(){
		System.out.println("Derived中的methodB()方法");
	}
	public void methodC(){
		methodA(); // 没有传参,访问父类中的methodA()
		methodA(20); // 传递int参数,访问子类中的methodA(int)
		methodB(); // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到
	}
}

总结

  • 通过子类对象访问父类与子类不同名的方法时,优先在子类中找,否则在父类中找
  • 通过子类对象访问父类与子类同名的方法时,若两种方法构成重载,根据调用参数的不同调用相应的方法,至于参数列表相同的情况,我们在后面解释到方法的重写的时候再详细解释

那么问题来了,如果在子类中,我偏要访问父类的成员呢?这时我们就需要引入下一个关键字。

8.1.5 super关键字

既然想要在子类中访问父类的成员,直接访问是无法做到的,这时,Java就为我们提供了super关键字,这个关键字的作用是:在子类方法中访问父类成员

public class Base {
	int a;
	int b;
	public void methodA(){
		System.out.println("Base中的methodA()");
	}
	public void methodB(){
		System.out.println("Base中的methodB()");
	}
}
public class Derived extends Base{
	int a; // 与父类中成员变量同名且类型相同
	char b; // 与父类中成员变量同名但类型不同
	// 与父类中methodA()构成重载
	public void methodA(int a) {
		System.out.println("Derived中的method()方法");
	}
	// 与基类中methodB()构成重写(即原型一致,重写后序详细介绍)
	public void methodB(){
		System.out.println("Derived中的methodB()方法");
	}
	public void methodC(){
		// 对于同名的成员变量,直接访问时,访问的都是子类的
		a = 100; // 等价于: this.a = 100;
		b = 101; // 等价于: this.b = 101;
		// 注意:this是当前对象的引用
		// 访问父类的成员变量时,需要借助super关键字
		// super是获取到子类对象中从基类继承下来的部分
		super.a = 200;
		super.b = 201;
		// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
		methodA(); // 没有传参,访问父类中的methodA()
		methodA(20); // 传递int参数,访问子类中的methodA(int)
		// 如果在子类中要访问重写的基类方法,则需要借助super关键字
		methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
		super.methodB(); // 访问基类的methodB()
		}
	}

注意事项

  • 只能在非静态方法中使用
  • 在子类方法中,访问父类的成员变量和方法会用到
  • super还有其他用法,后续介绍

8.1.6 子类构造方法

父子父子,先有父再有子,即:子类对象构造时,需要先调用父类的构造方法,然后再执行子类的构造方法

public class Base {
	public Base(){
		System.out.println("Base()");
	}
}
public class Derived extends Base{
	public Derived(){
		// super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),
		// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
		// 并且只能出现一次
		System.out.println("Derived()");
	}
}
public class Test {
	public static void main(String[] args) {
		Derived d = new Derived();
	}
}

在子类的构造方法中,并没有写任何关于父类的构造代码,但是在构造子类对象时,先执行了父类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。先有父再有子所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
注意

  • 如果父类,显式定义无参或者默认构造方法时,在子类构造方法的第一行有默认的super()调用,即调用父类的构造方法
  • 如果父类的构造方法是带有参数的,此时需要在子类构造方法中主动调用父类构造方法
  • 在子类构造方法中,super调用父类构造方法的语句必须放在子类构造方法的第一行
  • super只能在子类构造方法中出现一次,且不可以与this同时出现

8.1.7 super和this

【相同点】

  • 都是Java语言中的关键字
  • 只能在非静态方法中使用,用来访问非静态成员方法和字段
  • 在构造方法中调用时,必须是构造方法中的第一条语句,而且不能同时存在

【不同点】

  • this是指当前对象的引用,super相当于是子类对象从父类继承下来部分成员的引用
  • 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
  • 在构造方法中,==this调用本类的构造方法,super调用父类的构造方法,==但是两种方法不可以同时出现
  • 构造方法中一定会存在super调用,用户不写编译器会默认增加,但是this不写则没有

8.1.8 继承体系下的代码块

还记得我们之前讲的代码块吗,我们简单回顾,主要有两种代码块实例代码块和静态代码块,在没有继承关系下的执行顺序是:静态代码块>实例代码块

class Person {
	public String name;
	public int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
		System.out.println("构造方法执行");
	}
	{
		System.out.println("实例代码块执行");
	}
	static {
		System.out.println("静态代码块执行");
	}
}
public class TestDemo {
	public static void main(String[] args) {
		Person person1 = new Person("zhangsan",10);
		System.out.println("============================");
		Person person2 = new Person("lisi",20);
	}
}
  • 静态代码块先执行,并且只执行一次,在类加载阶段执行
  • 有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后执行构造方法
    (详细内容请参考前面的文章)

要是加入了继承关系呢?

class Person {
	public String name;
	public int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
		System.out.println("Person:构造方法执行");
	}
	{
		System.out.println("Person:实例代码块执行");
	}
	static {
		System.out.println("Person:静态代码块执行");
	}
}
class Student extends Person{
	public Student(String name,int age) {
	super(name,age);
		System.out.println("Student:构造方法执行");
	}
	{
		System.out.println("Student:实例代码块执行");
	}
	static {
		System.out.println("Student:静态代码块执行");
	}
}
public class TestDemo4 {
	public static void main(String[] args) {
		Student student1 = new Student("zhangsan",19);
		System.out.println("===========================");
		Student student2 = new Student("lisi",20);
		}
}

执行结果:
Person:静态代码块执行
Student:静态代码块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
===========================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行

经过上述分析,我们可以得出以下结论:

  • 父类静态>子类静态>父类实例>父类构造>子类实例>子类构造,原理就是,在java文件被写好之后,类就已经存在了,所以不管是父类还是子类,静态代码块都会被优先执行,且父类优先于子类,在之后实例化子类对象的时候,由于子类继承于父类,所以父类的实例代码块和构造方法优先被加载,创建一个对象的时候,先为对象分配内存空间,于是实例代码块被执行,之后调用构造方法,构造方法被执行,之后就是子类的实例代码块和构造方法
  • 第二次实例化的时候,父类和子类的静态代码块都不会再被执行,他们只执行一次,这是因为类只加载一次.

8.1.9 再谈访问限定符

再封装的章节,我们引入了访问限定符,主要限定:类或者类中成员能否再类外或者其他包中被访问

NO范围privatedefaultprotectedpublic
1同一包中的同一类
2同一包中的不同类×
3不同包中的子类××
4不同包中的非子类×××

这里记忆的时候,分为同一包和不同包,同一类和不同类,子类和非子类.

那么父类中不同访问权限的成员,在子类中的可见性怎么样呢?

// extend01包中
public class B {
	private int a;
	protected int b;
	public int c;
	int d;
}
// extend01包中
// 同一个包中的子类
public class D extends B{
	public void method(){
		// super.a = 10; // 编译报错,父类private成员在相同包子类中不可见
		super.b = 20; // 父类中protected成员在相同包子类中可以直接访问
		super.c = 30; // 父类中public成员在相同包子类中可以直接访问
		super.d = 40; // 父类中默认访问权限修饰的成员在相同包子类中可以直接访问
	}
}
// extend02包中
// 不同包中的子类
public class C extends B {
	public void method(){
		// super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见
		super.b = 20; // 父类中protected修饰的成员在不同包子类中可以直接访问
		super.c = 30; // 父类中public修饰的成员在不同包子类中可以直接访问
		//super.d = 40; // 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
	}
}
// extend02包中
// 不同包中的类
public class TestC {
	public static void main(String[] args) {
		C c = new C();
		c.method();
		// System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见
		// System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问
		System.out.println(c.c); // 父类中public成员在不同包其他类中可以直接访问
		// System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问
	}
}
  • 如上面的代码所示,想要在子类中访问到父类中的成员,在允许访问到的前提下,我们可以通过super访问到
  • 父类中的private成员虽然在子类中不可以访问到,但是也继承到了子类中
  • 注意在使用访问限定符时,要认证思考,不可以滥用访问限定符,考虑好类中的字段和方法提供给“谁”用,从而加上合适的访问限定符

8.1.10 继承方式

在Java中只支持以下几种继承方式
在这里插入图片描述
注意Java中不支持多继承
就像一个孩子不可以有多个父亲一样,但是一个父亲可以有多个孩子,就像上面的第三种

8.1.11 final关键字

我们有时不希望一个类被继承,我们希望在继承上进行限制,这时我们就需要用到final关键字
,而且final不仅可以修饰类,还可以用来修饰变量,成员方法

  1. 修饰变量或者字段,表示常量(即不可以被修改)
final int a=10;
//a=20 error
  1. 修饰类,表示一个类不可以被继承,称为密封类
final public class Animal{
//
}
//public class Cat extends Animal{
//
//} error
  1. 修饰方法:表示该方法不可以被重写

8.1.12 继承与组合

和继承类似,组合也是一种表达类之间关系的方式,也可以实现代码的复用。组合并没有用到什么关键字,也没有固定的语法格式,仅仅是将一个类的实例作为另外一个类的字段
继承表示的是一种is a的关系,比如猫是动物
而组合表示的是一种has a的关系,比如人有心脏
在这里插入图片描述

class Heart{
//
}
class Brain{
//
}
class Lungs{
//
}
class Person{
   private Heart heart;
   private Brain brain;
   private Lungs lungs;
}

组合和继承都可以实现代码的复用,该继承还是组合,一般看实际的应用场景

8.2 多态

8.2.1 多态的概念

通俗来说就是:多种形态,具体一点就是去完成某个行为,当不同的对象去完成的时候会产生出不同的效果
在这里插入图片描述
总的来说:同一件事情发生在不同对象的身上,就会产生不同的结果

8.2.2 多态实现的条件

Java中想要实现多态,必须要满足以下几个条件,缺一不可:

  • 必须在继承体系下
  • 子类必须要对父类中的方法进行重写
  • 通过父类引用调用重写的方法
    多态实现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
public class Animal {
	String name;
	int age;
	public Animal(String name, int age){
		this.name = name;
		this.age = age;
	}
	public void eat(){
		System.out.println(name + "吃饭");
	}
}
public class Cat extends Animal{
	public Cat(String name, int age){
		super(name, age);
	}
@Override
	public void eat(){
		System.out.println(name+"吃鱼~~~");
	}
}
public class Dog extends Animal {
	public Dog(String name, int age){
		super(name, age);
	}
	@Override
	public void eat(){
		System.out.println(name+"吃骨头~~~");
	}
}
public class TestAnimal {
	// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法
	// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法
	// 注意:此处的形参类型必须时父类类型才可以
	public static void eat(Animal a){
		a.eat();
	}
	public static void main(String[] args) {
		Cat cat = new Cat("元宝",2);
		Dog dog = new Dog("小七", 1);
		eat(cat);
		eat(dog);
	}
}

当类的调用者在编写eat这个方法的时候,参数类型为Animal,此时在方法内部并不关注当前的a指向的是那个类型的实例,此时a这个引用调用eat方法可能会有多种不同的表现,这种行为称为多态

8.2.3 重写

  1. 概念
    重写:也称为覆盖。重写就是对非静态方法,非private修饰的方法,非final修饰的方法,非构造方法的实现过程进行重新编写返回值和形参都不能改变,即外壳不变,实现核心重写,好处就是子类可以在父类的基础上实现自己的方法,定义特定于自己的行为
  2. 规则
  • 子类在重写父类的方法时,一般情况下返回值和形参都不能改变
  • 被重写的方法返回值类型有时可以不同,但是他们必须具有父子关系
  • 访问权限不可以比父类中被重写的方法更低,例如:父类方法被public修饰,则子类方法中重写该方法就不能声明为private
  • 父类方法不可以被static,final,private修饰,也不可以是构造方法,否者不可以被重写
  • 重写的方法,编译器会用“@override”来注解,可以检查合法性
  1. 重写和重载的区别
区别点重写重载
参数列表一定不可以修改必须修改
返回类型一定不可以修改(除非有父子关系)可以修改
访问限定符一定不可以降低权限可以修改

在这里插入图片描述

  1. 静态绑定与动态绑定
  • 静态绑定:也称为前期绑定,在编译时根据用户传递的参数类型就可以知道调用哪个方法,典型代表为方法的重载
  • 动态绑定:也称为后期绑定,在编译时,不可以确定方法的行为,无法知道调用哪个方法,需要等程序运行时才可以确定。

8.2.4 向上转型和向下转型

  1. 向上转型
    1. 概念:创建一个子类对象,把他当做父类对象来使用,就是范围从小到大的转换
    2. 语法格式:==父类类型 对象名=new 子类类型()
Animal animal=new Cat("miaomiao",2);

我们就拿上面的代码来说明:
在这里插入图片描述
3. 使用场景
- 直接赋值
- 方法传参
- 方法返回

public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
	public static void eatFood(Animal a){
		a.eat();
	}
// 3. 作返回值:返回任意子类对象
	public static Animal buyAnimal(String var){
		if("狗".equals(var) ){
			return new Dog("狗狗",1);
		}else if("猫" .equals(var)){
			return new Cat("猫猫", 1);
		}else{
			return null;
	}
}
public static void main(String[] args) {
	Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象
	Dog dog = new Dog("小七", 1);
	eatFood(cat);
	eatFood(dog);
	Animal animal = buyAnimal("狗");
	animal.eat();
	animal = buyAnimal("猫");
	animal.eat();
	}
}

通过上述的代码,我们不难发现向上转型的有限,他可以使得代码更加简单灵活,但是缺点也很明显,不可以调用子类特有的方法,怎么办呢,我们这里便引出了向下转型

  1. 向下转型
    将一个子类对象经过向上转型之后当做了父类的对象使用,无法再调用到子类的方法,但是我们有时候会想要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型

    1. 语法格式:子类类型 对象名=(子类类名)经过向上转型的父类对象
      从上述语法格式来看,其实就是强制类型转换
    2. 安全性问题
      向下转型存在不安全的问题,不同于向上转型,是安全的,比如原来一个对象是狗类,向上转为了动物类,再向下转型的时候必须转回狗类,不可以转为猫类
      在这里插入图片描述
    public class TestAnimal {
    	public static void main(String[] args) {
    		Cat cat = new Cat("元宝",2);
    		Dog dog = new Dog("小七", 1);
    		// 向上转型
    		Animal animal = cat;
    		animal.eat();
    		animal = dog;
    		animal.eat();
    		// 编译失败,编译时编译器将animal当成Animal对象处理
    		// 而Animal类中没有bark方法,因此编译失败
    		// animal.bark();
    		// 向上转型
    		// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
    		// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
    		cat = (Cat)animal;
    		cat.mew();
    		// animal本来指向的就是狗,因此将animal还原为狗也是安全的
    		dog = (Dog)animal;
    		dog.bark();
    	}
    }
    
    1. 解决方案
      使用instanceof判断一个对象所属于的类是否属于一个类的父类
    public class TestAnimal {
    	public static void main(String[] args) {
    	Cat cat = new Cat("元宝",2);
    	Dog dog = new Dog("小七", 1);
    	// 向上转型
    	Animal animal = cat;
    	animal.eat();
    	animal = dog;
    	animal.eat();
    	if(animal instanceof Cat){
    		cat = (Cat)animal;
    		cat.mew();
    	}
    	if(animal instanceof Dog){
    		dog = (Dog)animal;
    		dog.bark();
    	}
    }
    }
    

8.2.5 避免在构造方法中使用重写的方法

下面展示一段有坑的代码

class B {
	public B() {
		func();
	}
	public void func() {
		System.out.println("B.func()");
	}
}
class D extends B {
	private int num = 1;
	@Override
	public void func() {
		System.out.println("D.func() " + num);
	}
}
public class Test {
	public static void main(String[] args) {
		D d = new D();
	}
}

运行结果:D.func() 0
难道结果不应该是1吗,我们下面解释为什么不是1

在构造D对象的同时,会调用B的构造方法,在子类构造的时候,先构造父类。B的构造方法调用了func方法,此时会触发动态绑定,会调用D中的func,此时D还没有触发构造方法,D自身还没有构造,此时num处于未初始化状态,值为0,所以输出为0.
所以我们在构造方法中调用重写方法时一定要注意,在自己编程是尽量避免这种行为

9. 数组

9.1 数组的认识

声明:为什么要在这里讲数组,我的思路是这样的,因为Arrays(数组)本身就是一个类,创建数组时,我们可以用类与对象的思想来理解,而且这个类中有很多方法,对数组进行操作时,我们需要调用这些方法,我们为了大家看代码更加方便,所以我们选择在讲完类与对象和继承与多态之后再讲数组,避免了大家在数组的学习中出现“看不懂代码”的尴尬,再来大家在C语言中一定学过数组,理解起来会比较轻松,我们会与C语言中的数组进行比较,充分体现我们Java语言的面相对象性。

9.1.1 数组的基本概念

9.1.1.1 什么是数组

数组,可以看做是相同类型元素的集合。在内存中是一段连续的空间,就好像车库一样,有一块一块的空间,在这个空间中都停着同一类事物——汽车。

在这里插入图片描述
数组有以下几个特征:

  • 数组存放的元素其类型相同
  • 数组的空间是连续的
  • 每个空间都有自己的编号,起始位置的编号为0
9.1.1.2数组的创建与初始化
  1. 数组的创建
    数据类型[] 数组名=new 数据类型[数组大小]
    这里我们看到了关键字new,说明我们创建了一个数组对象,与C语言不同的是中括号的位置,其实我们可以把数据类型加上中括号当做一个整体来理解,中括号可以看做是数组的标志。所以我们把它理解为这个对象的类型为什么样数据类型的一个数组,这时你就会发现,怪的其实是C语言。
//举个例子
int[] array1=new int [10];
9.1.1.3数组的初始化

数组的初始化分为动态初始化和静态初始化

  1. 动态初始化:在创建数组的时候,直接指定数组中的元素个数,就是我们上面创建数组的方法
  2. 静态初始化:在创建数组的时候不指定数组的个数,而是直接将具体的内容进行指定
    语法格式:数据类型[] 数组名称={data1,data2,data3…}
    或者我们也可以使用类与对象的思想来创建:数据类型[] 数组名称=new 数据类型[] {data1,data2,…}
int[] array1 = new int[]{0,1,2,3,4,5,6,7,8,9};
double[] array2 = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = new String[]{"hell", "Java", "!!!"};

【注意事项】

  • 静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中的元素个数来指定数组长度
  • 静态初始化时,{}中的数据必须与[]前的数据类型一致
  • 静态初始化时,虽然可以省略new,但是编译器在编译时会把new还原出来
  • 在创建和初始化分开时,静态初始化不可以省略new
int[] array1;
array1 = new int[10];
int[] array2;
array2 = new int[]{10, 20, 30};
// 注意省略格式不可以拆分, 否则编译失败
// int[] array3;
// array3 = {1, 2, 3};
  • 如果没有对数组进行初始化,数组中有默认值
类型默认值
byte0
short0
int0
long0
float0.0f
double0.0
char/u0000
booleanfalse
9.1.1.4数组的使用
  1. 数组的访问
    数组可直接通过其下标来访问
int[]array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
// 也可以通过[]对数组中的元素进行修改
array[0] = 100;
System.out.println(array[0]);

注意:数组不可以越界访问,必须介于[0,N)之间
2. 数组的遍历
所谓遍历,就是将数组的元素全部访问一遍,若我们有以下代码:

int[]array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);

上述代码的问题是:一条一条地输出语句显得非常麻烦,所以我们考虑使用循环语句

int[]array = new int[]{10, 20, 30, 40, 50};
for(int i = 0; i < 5; i++){
	System.out.println(array[i]);
}

上述代码的问题是,我们如果花括号中有好多好多元素,我们要一个一个地数吗,当然不是,Arrays类中为我们提供了非常便利的一个方法:length方法,我们可以通过数组名.length来获取到数组的长度

int[]array = new int[]{10, 20, 30, 40, 50};
for(int i = 0; i < array.length; i++){
	System.out.println(array[i]);
}

也可以通过for-each遍历数组

int[] array = {1, 2, 3};
for (int x : array) {
	System.out.println(x);
}

也可以将数组转化为字符串进行输出

System.out.println(Arrays.toString(array));

这里通过Arrays的类名来调用了toString方法,在底层代码中,其实是Arrays类对他的父类Object类的toString方法进行了重写,所以才有了这么方便的一个方法。

9.1.2 组是引用类型

9.1.2.1基本类型和引用类型在底层上的区别

基本类型在栈中创建的变量,数值直接存放在栈上,例如:int,char,byte等,但是引用类型不是,在栈上存放的是一个引用类型对象的地址,可以理解为首元素地址,我们一般称它为对象的引用,真正的内容存放在堆中
在这里插入图片描述

9.1.2.2认识null

null 在 Java 中表示 “空引用” , 也就是一个不指向对象的引用.

int[] arr = null;
System.out.println(arr[0]);

执行结果
Exception in thread “main” java.lang.NullPointerException
at Test.main(Test.java:6)

null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操
作. 一旦尝试读写, 就会抛出 NullPointerException.这个东西叫做空指针异常,我们在后面介绍异常的时候详细解释

9.1.3 数组的使用场景

  1. 保存数据
public static void main(String[] args) {
	int[] array = {1, 2, 3};
	for(int i = 0; i < array.length; ++i){
		System.out.println(array[i] + " ");
	}
}
  1. 作为方法的参数
    这点与C语言不同,既然数组数组是引用数据类型,既然是数据类型,就可以作为方法的参数
public static void main(String[] args) {
	int[] arr = {1, 2, 3};
	func(arr);
	System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
	a[0] = 10;
	System.out.println("a[0] = " + a[0]);
}

执行结果
a[0] = 10
arr[0] = 10

发现在func方法内部修改数组的内容, 方法外部的数组内容也发生改变.
因为数组是引用类型,按照引用类型来进行传递,是可以修改其中存放的内容的。底层图解如下:
在这里插入图片描述
【总结】所谓的 “引用” 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中.

  1. 作为方法的返回类型
    比如获取斐波那契数列的前N项:
public class TestArray {
	public static int[] fib(int n){
	if(n <= 0){
		return null;
	}
	int[] array = new int[n];
	array[0] = array[1] = 1;
	for(int i = 2; i < n; ++i){
		array[i] = array[i-1] + array[i-2];
	}
	return array;
}
public static void main(String[] args) {
	int[] array = fib(10);
	for (int i = 0; i < array.length; i++) {
		System.out.println(array[i]);
		}
	}
}

9.2 数组的应用与常用方法

9.2.1 数组转字符串

int [] array = {0,1,2,3,4}
System.out.println(Arrays.toString(array));
//调用Arrays类中重写的toString方法
//执行结果:[0,1,2,3,4]

这里使用了Arrays类中重写的toString方法,源码如下:
为了大家方便阅读代码,我们在其中添加注释

public static String toString(int[] a) {
        if (a == null)
            return "null";//数组为null返回null
        int iMax = a.length - 1;
        if (iMax == -1)//数组为空返回空数组(a.length==0)
            return "[]";

        StringBuilder b = new StringBuilder();//创建StringBuilder
        b.append('[');//首先为b添加“[”
        for (int i = 0; ; i++) {
            b.append(a[i]);//遍历数组,为b添加元素
            if (i == iMax)
                return b.append(']').toString();//遍历到数组最后停止,并添加“]”
            b.append(", ");
        }
    }

9.2.2 数组拷贝

public static void func(){
	// newArr和arr引用的是同一个数组
	// 因此newArr修改空间中内容之后,arr也可以看到修改的结果
	int[] arr = {1,2,3,4,5,6};
	int[] newArr = arr;
	newArr[0] = 10;
	System.out.println("newArr: " + Arrays.toString(arr));
	// 使用Arrays中copyOf方法完成数组的拷贝:
	// copyOf方法在进行数组拷贝时,创建了一个新的数组
	// arr和newArr引用的不是同一个数组
	arr[0] = 1;
	newArr = Arrays.copyOf(arr, arr.length);
	System.out.println("newArr: " + Arrays.toString(newArr));
	// 因为arr修改其引用数组中内容时,对newArr没有任何影响
	arr[0] = 10;
	System.out.println("arr: " + Arrays.toString(arr));
	System.out.println("newArr: " + Arrays.toString(newArr));
	// 拷贝某个范围.
	int[] newArr2 = Arrays.copyOfRange(arr, 2, 4);
	System.out.println("newArr2: " + Arrays.toString(newArr2));
}

我们前面说到,数组类型的对象是引用对象,所以我们通过画堆和栈的方法来解释以上的代码:

  • 第一段代码:
    在这里插入图片描述
  • 第二段代码
    在这里插入图片描述
  • 第三段代码
    在这里插入图片描述
    以下是关于copyOf和copyOfRage的源码:
    我们还是通过注释解释
public static int[] copyOf(int[] original, int newLength) {//原数组和新数组的长度
        int[] copy = new int[newLength];//创建新数组,所以对原来的数组没有影响
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));//该行代码实现源码比较复杂,这里不多解释
        return copy;//返回复制后的数组
    }

在后期我们学习数据结构的时候,会反复使用该方法对顺序表,优先级队列,栈等的数据结构进行扩容。
我们下面使用一个简单的方法实现一个自己的copyOf方法

 public int[] myCopyOf(int[] array,int newLength){
        int [] newArray = new int[newLength];
        for (int i = 0; i < array.length; i++) {
            newArray[i] = array[i];
        }
        return newArray;
    }

9.2.3 求数组中的平均值

public static void main(String[] args) {
	int[] arr = {1,2,3,4,5,6};
	System.out.println(avg(arr));
}
public static double avg(int[] arr) {
	int sum = 0;
	for (int x : arr) {
	sum += x;
	}
	return (double)sum / (double)arr.length;//注意强制类型转换
}
// 执行结果
//3.5

9.2.4 顺序查找数组元素

public static void main(String[] args) {
	int[] arr = {1,2,3,10,5,6};
	System.out.println(find(arr, 10));
}
public static int find(int[] arr, int data) {
	for (int i = 0; i < arr.length; i++) {
	if (arr[i] == data) {
		return i;
		}
	}
	return -1; // 表示没有找到
}
// 执行结果
//3分为

上述代码在查找上有缺陷,这种方法很慢,我们下面介绍一种比较高效的方法

9.2.5 二分查找数组元素

这种方法虽然相较顺序查找比较高效,但是也有一定的限制,这种方法只对有序数组生效,那么什么叫有序数组?有序数组分为升序和降序,如:1 2 3 4, 4 3 2 1,我们以升序数组为例
二分查找的大体思路为:

  • 把索要查找的元素和中间的元素进行比较
  • 若等于中间元素,则要查找的元素就是中间元素
  • 若小于中间元素,则去数组左侧查找
  • 若大于中间元素,则去数组右侧查找
public static void main(String[] args) {
	int[] arr = {1,2,3,4,5,6};
	System.out.println(binarySearch(arr, 6));
}
public static int binarySearch(int[] arr, int toFind) {
	int left = 0;
	int right = arr.length - 1;
	while (left <= right) {
	int mid = (left + right) / 2;
	if (toFind < arr[mid]) {
	// 去左侧区间找
	right = mid - 1;
	} else if (toFind > arr[mid]) {
	// 去右侧区间找
	left = mid + 1;
	} else {
	// 相等, 说明找到了
	return mid;
	}
	}
	// 循环结束, 说明没找到
	return -1;
	}
	// 执行结果
	//5

9.2.6 数组的排序

给定一个数组,我们来将这个数组排序,排列为升序或降序,假设我们要排升序

  1. 我们排序时可以使用冒泡排序法,当然,冒泡排序法只是其中一种方法,在后续学习数据结构时,我们会学习更多排序方法,例如:堆排序,快速排序等

冒泡排序大体思路:

  • 确定排序趟数
  • 确定每一趟排序的次数
  • 第一趟,从开头开始,将相邻元素两两比较,第二趟,从第二个元素开始,重复第一次的操作…

我们通过一个动图来形象地描述冒泡排序:
在这里插入图片描述
我们通过代码来实现:

public static void main(String[] args) {
	int[] arr = {9, 5, 2, 7};
	bubbleSort(arr);
	System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
	for (int i = 0; i < arr.length; i++) {
		for (int j = 1; j < arr.length-i; j++) {
			if (arr[j-1] > arr[j]) {
			int tmp = arr[j - 1];
			arr[j - 1] = arr[j];
			arr[j] = tmp;
			}
		}
	} // end for
} // end bubbleSort
// 执行结果
//[2, 5, 7, 9]
  1. 我们也可以使用选择排序来排序

大体思路:

  • 先把第一个数据作为最小数据,之后一一与后面的元素进行比较
  • 若遇到比第一个元素小的元素,则替换最小元素
  • 遍历到最后,将第一个与最小元素交换
  • 走到第二个元素,重复上述操作
    我们依然通过动图来解释:
    在这里插入图片描述
    代码如下:
 public int[] selectSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            int j = i+1;
            for (; j < array.length; j++) {
                if(array[j]<array[minIndex]){
                    minIndex = j;
                }
            }
            int tmp = array[minIndex];
            array[minIndex]=array[i];
            array[i] = tmp;
        }
        return array;
    }

冒泡排序性能较低. Java 中内置了更高效的排序算法,即sort方法

public static void main(String[] args) {
	int[] arr = {9, 5, 2, 7};
	Arrays.sort(arr);
	System.out.println(Arrays.toString(arr));
}

Java系统中实现sort方法的底层排序方法是快速排序,我们后续介绍

9.2.7 数组逆序

给定一个数组, 将里面的元素逆序排列.
思路:

  • 设定两个下标, 分别指向第一个元素和最后一个元素. 交换两个位置的元素.
  • 然后让前一个下标自增, 后一个下标自减, 循环继续即可.

代码示例:

public static void main(String[] args) {
	int[] arr = {1, 2, 3, 4};
	reverse(arr);
	System.out.println(Arrays.toString(arr));
}
public static void reverse(int[] arr) {
	int left = 0;
	int right = arr.length - 1;
	while (left < right) {
		int tmp = arr[left];
		arr[left] = arr[right];
		arr[right] = tmp;
		left++;
		right--;
	}
}

9.2.8 二维数组

二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组

  • 基本语法:
    数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
  • 代码实例
int[][] arr = {
	{1, 2, 3, 4},
	{5, 6, 7, 8},
	{9, 10, 11, 12}
	};
for (int row = 0; row < arr.length; row++) {
	for (int col = 0; col < arr[row].length; col++) {
		System.out.printf("%d\t", arr[row][col]);
	}
	System.out.println("");
}
// 执行结果
1 2 3 4
5 6 7 8
9 10 11 12

二维数组在后面其实使用的比较少,我们在后面使用二维数组时一般都是通过顺序表结合泛型来创建

10.抽象类与接口

10.1 抽象类

10.1.1抽象类的概念

在面向对象的概念中,所有的对象都是通过类来描述的,但是有时候一些类并不可以清晰地刻画一个对象,没有包含足够的信息来描绘一个具体的对象的类,那么这样的类就叫做抽象类,比如说:有一个类,这个类是一个图形,但是仅凭借这些信息,你现在完全不会在纸上描绘出一个图形,因为你不知道这个图形是一个什么样的图形,图形包括好多,有正方形,三角形等等

在这里插入图片描述
从上面的信息,我们不难看出以下几点:

  • 这些具体的图形与抽象的图形是继承关系
  • 如果抽象图形中有一个画图形的方法,这个方法无法具体实现
  • 子类中的画图形方法可以实现

在这个例子中,我们令抽象图形类为Shape,令画图形的方法为draw,则Shape类为抽象类,其中的draw方法不能实现任何的具体功能,是一个没有实际功能的方法,我们称这样的方法为抽象方法

10.1.2抽象类的语法

在Java中,如果已给类被abstract修饰,我们称这个类为抽象类,抽象类中被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现,我们拿上面的Shape类来举例:

//抽象类被abstract修饰
public abstract class Shape {
    protected double area;//抽象类中也可以包含普通成员变量
    public abstract void draw();//抽象类的方法中必须也是abstract修饰
    abstract void area();
    public double getArea(){//普通成员方法
        return area;
    }
}

注意:抽象类也是类,内部也可以包含普通方法和普通成员变量,甚至可以是构造方法

抽象类与普通类的不同于相同

  • 都可以包含普通成员变量与成员方法
  • 抽象类比普通类多了抽象方法和抽象
  • 抽象类不可以被实例化

10.1.3抽象类的特性

  1. 抽象类不可以直接实例化对象
  2. 抽象方法不可以是private,static,final修饰的,因为它必须被子类重写
  3. 抽象类必须被继承,并且在继承之后必须重写抽象方法,否则子类也需要用abstract修饰,即子类也是一个抽象类,类似于后面接口的继承,我们之后会聊到.
    我们下面拿一个具体的例子来说明:
public abstract class Shape {
    protected double area;//抽象类中也可以包含普通成员变量
    public abstract void draw();//抽象类的方法中必须也是abstract修饰
    abstract void area();
    public double getArea(){//普通成员方法
        return area;
    }
}
public class Rect extends Shape {
    private double wide;//宽
    private double length;//长
    private double area;//面积

    public Rect(double wide, double length) {
        this.wide = wide;
        this.length = length;
    }
    public void area() {//重写父类area方法,计算面积
        this.area = wide * length;
    }
    public void draw(){//重写父类draw方法,画图形
        System.out.println("画一个矩形");
    }
}

  1. 抽象类中不一定含有抽象方法,但是抽象方法一定包含在抽象类中
  2. 抽象类中可以有构造方法,供子类创建对象时,初始化父类成员变量,注意并不是用来实例化时调用,因为它就不可以被实例化

10.1.4 抽象类的作用

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验.
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
充分利用编译器的校验, 在实际开发中是非常有意义的.

10.2 接口

10.2.1接口的概念

在我们现实生活中,我们到处可以见到各种各样的接口,比如:笔记本电脑上的USB接口
电源插座等
在这里插入图片描述
在这里插入图片描述
电脑上的USB接口可以插:键盘,鼠标,u盘等所有符合USB协议的设备
插座上可以插电源:电饭煲,电脑,电视机等所有符合用电规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用
在Java中,接口可以看做:多个类公共的规范,是一种引用类型的数据

10.2.2语法规则

接口的定义格式与定义类的格式基本相同,将class关键字替换为interface关键字,就定义了一个接口

public interface Itest{
// 抽象方法
	public abstract void method1(); // public abstract 是固定搭配,可以不写
	public void method2();
	abstract void method3();
	void method4();
	// 注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}

注意:创建接口时,接口的名称一般用大写字母I开头
虽然以上代码中加上一些abstract,public等的一些修饰符不会出现语法错误,但是我们嫩为了保证代码的简洁性,我们建议接口中的方法前面不要加任何修饰符

10.2.3 接口的使用

接口不可以直接使用,必须有一个“实现类”来实现该接口,类与接口之间是implements的关系,这也是一个类实现一个接口的关键字,就和上面一个类继承一个抽象类道理是相同的,语法格式如下:

public class 类名 implements 接口名

注意:子类和父类是extends关系,接口和类是implements关系
下面我们来具体举一个例子,我们拿USB接口来说事:

//USB接口
public interface USB {
    void openService();
    void closeService();
}
public class Mouse implements USB{
    @Override
    public void openService() {
        System.out.println("插入鼠标");
    }

    @Override
    public void closeService() {
        System.out.println("拔出鼠标");
    }
    public void click(){
        System.out.println("点击鼠标");
    }
}
public class KeyBoard implements USB{
    @Override
    public void openService() {
        System.out.println("插入键盘");
    }

    @Override
    public void closeService() {
        System.out.println("拔出键盘");
    }
    public void inPut(){
        System.out.println("打字~");
    }
}

10.2.4接口特性

  1. 接口是一种引用类型,但是不可以被实例化
    在这里插入图片描述

  2. 接口中每一个方法都是public修饰,即每一个方法都会被隐式地定义为public abstract,若添加其他修饰符,都会报错
    在这里插入图片描述

  3. 接口中的方法是不可以在接口中实现的,只能由类来实现
    在这里插入图片描述

  4. 重写接口的方法时,不可以使用比public修饰符权限更低的修饰符来重写方法,这和继承体系中的重写是一样的规则
    在这里插入图片描述

  5. 接口中可以含有变量,但是接口中的变量会被隐式的定义为public abstract final修饰

  6. 接口中不可以含有任何的代码块和构造方法,但是抽象类可以有
    在这里插入图片描述
    在这里插入图片描述

  7. 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class

  8. 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类

  9. jdk8中:接口中还可以包含default方法

10.2.5 实现多个接口

在Java语言中,类和类之间是单继承关系,一个类只可以有一个父类,即Java中不支持多继承关系但是一个类可以实现多个接口,下面通过Animal类来具体说明

class Animal {
	protected String name;
	public Animal(String name) {
		this.name = name;
	}
}

另外我们再提供一组接口,分别表示这个动物会跑,会游泳,会飞

interface IFlying {
	void fly();
}
interface IRunning {
	void run();
}
interface ISwimming {
	void swim();
}

接下来是具体的动物类,比如青蛙是两栖动物,既会跑,有会游泳

class Frog extends Animal implements IRunning, ISwimming {//可以实现多个接口
	public Frog(String name) {
		super(name);
	}
@Override
	public void run() {
		System.out.println(this.name + "正在往前跳");
	}
@Override
	public void swim() {
		System.out.println(this.name + "正在蹬腿游泳");
	}
}

注意:在一个类实现多接口的时候,每一个接口的方法都必须被重写,我们在idea编译器中,可以使用Ctrl+i快速实现接口

再比如鸭子,既会跑,又会游泳,又会飞

class Duck extends Animal implements IRunning, ISwimming, IFlying {
	public Duck(String name) {
		super(name);
	}
@Override
	public void fly() {
		System.out.println(this.name + "正在用翅膀飞");
	}
@Override
	public void run() {
		System.out.println(this.name + "正在用两条腿跑");
	}
@Override
	public void swim() {
		System.out.println(this.name + "正在漂在水上");
	}
}

上面展示了Java中面相对象中最常见的一种用法:一个类继承一个父类,同时实现多个接口
继承表达的含义是is a,即一个类属于什么大类,接口则表示的是can xxx,即一个类具有什么样的特性或能力
这样设计的好处在哪里呢?时刻牢记多态的好处,它可以让程序员忘记类型,有了接口之后,就不必关心具体的类型,而是关注某个类是否具有某种能力

面试题:什么时候会发生多态

在不同类继承一个父类,并且重写了父类中的方法,并且通过父类引用创建的子类对象调用了重写的方法,此时会发生动态绑定并发生向上转型,此时就会发生多态

只要这个类的东西具有某种能力,就可以实现一个接口,比如实现上述接口的类不一定必须是动物类,还可以是其他类,比如机器人也会跑

class Robot implements IRunning {
	private String name;
	public Robot(String name) {
		this.name = name;
	}
	@Override
	public void run() {
		System.out.println(this.name + "正在用轮子跑");
	}
}

10.2.6 接口之间的继承

在Java中,类和类之间是单继承关系,一个类可以实现多个接口,接口与接口之间可以多继承,即接口可以达到多继承的目的
接口可以继承一个接口,达到复用的效果,使用extends关键字

interface IRunning {
	void run();
}
interface ISwimming {
	void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {

}
class Frog implements IAmphibious {

}

通过一个创建一个新接口IAmphibious,并继承了IRunning, ISwimming,来达到合并两个接口的目的,在Frog类中,还需要继续重写run方法和swim方法

10.2.7 接口的经典使用案例

  1. 给对象数组排序
    如果我们有一个数组,这个数组中的数据类型是学生类
public class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        Student[] student = new Student[]{
                new Student("zhangsan",95),
                new Student("lisi",89),
                new Student("wangwu",88),
                new Student("zhaoliu",98),
        };
        }
    }

比如我们在这里要对这个数组中的学生按照成绩进行排序,数组中我们有sort方法,能否直接使用这个方法呢?
在这里插入图片描述
在这里我们可以看出,编译器在这个地方抛出了异常,为什么呢,是因为一个数组中的对象有两个成员,一个是姓名,一个是成绩,编译器并不知道你要通过哪个成员来比较对象,所以我们这里引入了Comparable接口,通过重写该接口中的compareTo方法来实现对象的比较

public class Student implements Comparable {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    @Override
    public int compareTo(Object o) {//重写compareTo方法
        Student s = (Student) o;//object类强转为Student类
        if (this.score > s.score){
            return -1;
        }else if (this.score<s.score){
            return 1;
        }else {
            return 0; 
        }
    }
}

在sort方法中,会自动调用重写的compareTo方法
然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0;
之后的运行就符合预期了
在这里插入图片描述
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.

在这里我们给出几种更加灵活的方法,利用比较器,即实现Compartor接口

public class nameCompare implements Comparator<Student> {//比较什么,尖括号里就写什么
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}
public class sorceCompare implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        if (o1.score > o2.score){
            return -1;
        }else if (o1.score<o2.score){
            return 1;
        }else {
            return 0;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Student[] student = new Student[]{
                new Student("zhangsan",95),
                new Student("lisi",89),
                new Student("wangwu",88),
                new Student("zhaoliu",98),
        };
        Arrays.sort(student);
        System.out.println(Arrays.toString(student));
        nameCompare comparator1 = new nameCompare();
        sorceCompare comparator2 = new sorceCompare();
        System.out.println(comparator1.compare(student[0],student[1]));
        System.out.println(comparator2.compare(student[0],student[1]));
        }
}

在这里插入图片描述
在这里我们可以看出,与Comparable接口不同的是,它不是在想要比较的类上直接添加,而是单独创建了一个类并创建对象,可以根据成绩比较,可以根据名字比较,返回的是一个整数,若我们想利用比较器对数组进行排序,可不可以实现呢,当然可以,我们在上述代码的基础上稍作改动

public class Main {
    public static void main(String[] args) {
        Student[] student = new Student[]{
                new Student("zhangsan",95),
                new Student("lisi",89),
                new Student("wangwu",88),
                new Student("zhaoliu",98),
        };
        Arrays.sort(student);
        System.out.println(Arrays.toString(student));
        nameCompare comparator1 = new nameCompare();
        sorceCompare comparator2 = new sorceCompare();
        System.out.println(comparator1.compare(student[0],student[1]));
        System.out.println(comparator2.compare(student[0],student[1]));
        Arrays.sort(student,comparator1)//传入比较器
        System.out.println(Arrays.toString(student));
        }
    }

在数组类的sort方法中,我们传入了一个关于名字的比较器,是sort的一个重载方法,那么运行结果就会根据名字字母的先后顺序进行排序,运行结果如下
在这里插入图片描述
在这里我们给出重载sort方法的源码,方便大家理解

 public static <T> void sort(T[] a, Comparator<? super T> c) {//在这里我们可以看到该方法有比较器的形式参数
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

10.2.8抽象类与接口的区别

抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题).
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.

class Animal {
	protected String name;
	public Animal(String name) {
		this.name = name;
	}
}

10.3 拷贝与Object类

10.3.1 Object类

10.3.1.1 概述

Object类是Java默认提供的一个类,在Java里面除了Object类,与所有的类都存在继承关系,他们都会默认继承Object类即所有的对象都可以用Object类来接收,它可以向上转所有类型的对象

class Person{}
class Student{}
public class Test{
	public static void main(String[] args){
		func(new Person());
		func(new Student());
	}
	public static void func(Object obj){
	System.out.println(obj);
	}
}

问题:
如果所有类的默认继承了Object类,那么一个类在手动继承了一个类,就相当于继承了两个类,那么为什么不报错呢

其实并不是这样的原理,是手动继承的父类已经继承了Object类,子类再去手动继承父类就相当于多层继承,这种继承方式是允许的

在开发的过程中,Object类是最高统一的类型,是所有类型的父类,但是Object类也有一些定义好的方法,我么来通过jdk1.8帮助手册来查看:
在这里插入图片描述
现阶段我们只需要掌握用记号笔画注的几个方法,Object类的方法时需要全部掌握的,其他的方法我们在后面再慢慢接触

10.3.1.2 toString方法

如果要打印对象中的内容,可以直接重写Object类中的toString方法,之前我们用过好几次了
object中的toString:

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

学生类中重写:

public class Student implements Comparable {
    public String name;
    public int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
 }

这是之前Student类的例子

重写方法可以用alt+insert来快速实现
在这里插入图片描述
在这里插入图片描述

10.3.1.3 对象比较equals方法
  1. 在Java中,如果“==”两边是基础类型,那么可以直接比较他们的数值是否相等
  2. 如果“==”两边是引用类型,那么比较的是他们的地址是否相同
  3. 如果我们要比较对象中的内容是否相同,我们则需要重写equals方法

Object中的equals方法:

public boolean equals(Object obj) {
        return (this == obj);//使用引用中的地址直接进行比较,只看它们是不是同一个引用
    }
public class Student {
    public String name;
    public int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null){//传入null
            return false;
        } else if (this == obj) {//同一个引用
            return true;
        }
        if (!(obj instanceof Student)){//若传入的对象不是学生类
            return false;
        }
        Student stu = (Student) obj;//把obj对象向下转回来,以便比较
        return stu.name.equals(this.name)&&stu.score == this.score;//String类重写了equals方法,可直接比较字符串是否相同
    }
}

下面是String类中重写的equals方法:

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        return (anObject instanceof String aString)
                && (!COMPACT_STRINGS || this.coder == aString.coder)
                && StringLatin1.equals(value, aString.value);
    }

总结:比较引用内容是否相同,需要重写equals方法

10.3.1.4 hashcode方法

hashcode()这个方法,可以理解为它为你算出了一个对象的具体位置,目前可以理解为地址,其实这牵扯到了数据结构中的hash表,关键字和hashcode形成了某种映射关系,我们讲到后面再细说
hashcode的源码:

public native int hashCode();

这个方法时native方法,底层是c/c++代码实现的,所以我们看不到
我们还是以学生类来举例,若我们不重写hashcode方法

public class Main {
    public static void main(String[] args) {
        Student[] student = new Student[]{
                new Student("zhangsan",95),
                new Student("zhangsan",95)
        };
        System.out.println(student[0].hashCode());
        System.out.println(student[1].hashCode());
        }
    }

在这里插入图片描述
在这里我们可以看到,这两个值是完全不同的两个值,虽然他们的成绩和名字是一样的
现在我们重写hashcode方法

public class Student{
    public String name;
    public int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, score);
    }
}
public class Main {
    public static void main(String[] args) {
        Student[] student = new Student[]{
                new Student("zhangsan",95),
                new Student("zhangsan",95)
        };
        System.out.println(student[0].hashCode());
        System.out.println(student[1].hashCode());
        }
    }

在这里插入图片描述
现在我们发现两个对象的hashcode值是一样的

在我们重写equals和hashcode方法的时候仍然可以通过alt+insert方法快速生成
在这里插入图片描述

10.3.2 Clonable接口与深拷贝浅拷贝

Java中内置了一些很有用的接口,上面的Comperable接口就是其中之一,接下来我们就介绍的Clonable接口也是其中之一
在object类中有一个clone方法,调用这个方法可以对对象进行拷贝,但是这个方法不可以直接使用,我们必须在被拷贝数据类型的后面实现Clonable接口,否者会抛出CloneNotSupportedException的异常
现在我们看一下Clonable接口的源码:

public interface Cloneable {
}

在这里我们惊奇地发现这个接口是一个空接口,这里我们需要解释以下,Clonable接口在这里只起到标记的作用,证明这个类是可以被拷贝的,我们称它为标记接口
接下来我们便可以用clone方法实现一个浅拷贝:

public class Money {
    public double money = 99.9;
}
public class Student implements Cloneable{
    public String name;
    public int score;
    public Money m;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
        this.m = new Money();//在该类中创建一个对象,否则为null
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                ", m=" + m.money +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Main  {
    public static void main(String[] args) throws CloneNotSupportedException {//在主函数中要对异常进行声明,否者在调用clone方法时会抛出之前的异常
        Student s1 = new Student("zhangsan",99);
        Student s2 = (Student) s1.clone();//clone一个学生出来
        System.out.println(s1);
        System.out.println(s2);
        }
}

运行结果如下:
在这里插入图片描述
如今我们对s2的m进行修改:

public class Main  {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("zhangsan",99);
        Student s2 = (Student) s1.clone();
        System.out.println(s1);
        System.out.println(s2);
        s2.m.money = 88.8;
        System.out.println(s1);
        System.out.println(s2);
        }
}

运行结果如下:
在这里插入图片描述

我们发现,s1的m也被修改了,为什么呢,我们通过图来解释:
在这里插入图片描述

从上述的图中可以看出,m对象没有进行拷贝,两个引用都是指向了同一个对象上述代码只是进行了浅拷贝,并没有对m进行拷贝所以在修改s2的m时必定会把s1的m也修改掉,下面我们对s1进行深拷贝

public class Money implements Cloneable{
    public double money = 99.9;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();//把Money类也设置为客克隆的类,并提供重写方法
    }
}
public class Student implements Cloneable{
    public String name;
    public int score;
    public Money m;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
        this.m = new Money();//在该类中创建一个对象,否则为null
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                ", m=" + m.money +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student tmp = (Student) super.clone();//创建一个临时变量,把studen拷贝过来
        tmp.m = (Money) this.m.clone();//把临时变量中的m对象另外拷贝一份
        return tmp;//返回临时变量
    }
}
public class Main  {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("zhangsan",99);
        Student s2 = (Student) s1.clone();
        System.out.println(s1);
        System.out.println(s2);
        s2.m.money = 88.8;
        System.out.println(s1);
        System.out.println(s2);
        }
    }

运行结果如下:
在这里插入图片描述
从这里我们可以看出,s1的m值没有被改变,接下来我们画图解释原理:
在这里插入图片描述
从上述图中我们可以看出,m也进行了靠北,所以我们便完成了深拷贝

11. String类

11.1 常用方法

11.1.1 字符串构造

String类的常用构造方法只有以下三种

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";//使用常量串进行构造
        String s2 = new String("hello");//创建String对象
        char[] array = {'h','e','l','l','o'};//使用字符数组
        String s3 = new String(array);
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
    }
}

如要使用其他String方法,大家可以查看源码或者是jdk帮助手册
【注意】

  1. String类是引用类型的对象内部其实并不存在字符串本身,我们可以查看String类的部分源码:
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
     private final byte[] value;//用于存字符串的数组,字符存储在这个数组中形成了字符串
     private int hash; // 默认为0
     public String(String original) {//对以上两个成员变量进行构造
        this.value = original.value;
        this.hash = original.hash;
        }
  }

下面我们用一段代码来说明String是引用类型

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("world");
        char[] array = {'h','i'};//创建了3个不同的引用对象
        String s3 = new String(array);
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        String s4 = s3;//s4和s3指向了同一个引用
        System.out.println(s4);
    }
}

字符串的引用方式其实和我们前面提到的数组很类似,因为底层就是数组实现的,我们下面通过画图说明
在这里插入图片描述
2. 在Java中,直接用双引号引起来的对象也是String类

System.out.println("hello");

11.1.2 字符串的常规方法

  1. 计算长度
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("world");
        char[] array = {'h','i'};//创建了3个不同的引用对象
        String s3 = new String(array);
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        String s4 = s3;//s4和s3指向了同一个引用
        System.out.println(s4);
        System.out.println(s3.length());
    }
}

在这里插入图片描述
我们在这里需要注意的一点是,Java中的字符串不想c语言一样有以‘\0’这样的转义字符结尾的说法有几个字母,字符串就是多长,在上面的运行结果我们可以看到,s3的长度为2

  1. 判空
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("");//s2为空字符
        char[] array = {'h','i'};//创建了3个不同的引用对象
        String s3 = new String(array);
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        String s4 = s3;//s4和s3指向了同一个引用
        System.out.println(s4);
        System.out.println(s3.length());
        System.out.println(s2.isEmpty());
    }
}

在这里插入图片描述
从上述结果我们可以看出,isEmpty方法如果字符串为空,返回ture,如果不为空,返回false

11.1.3 String对象的比较

字符串的比较也是常见的操作之一,比如:字符串的排序,Java中总共提供了4种方式

  1. == 比较是否引用同一个对象
    注意:对于内置类型,比较的是变量中的值,对于引用类型比较的是引用中的地址
public class Main {
    public static void main(String[] args) {
        int a = 1;
        int b = 1;
        int c = 2;
        String s1 = "hello";
        String s2 = "hi";
        String s3 = "hello";
        String s4 = s1;
        System.out.println(a == b);//两个值相等
        System.out.println(a == c);//两个值不相等
        System.out.println(s1 == s2);//两个字符串不一样
        System.out.println(s3 == s1);//虽然两个字符串相同,但是指向的引用不同
        System.out.println(s1 == s4);//指向同一个引用
    }
}

在这里插入图片描述

需要注意的是,由于String是引用类型,虽然s1和s3相同,但是它们的引用不同,所以返回false
那么我们如果想要比较两个字符串是否相同,我们有办法吗,当然有,我们下面来介绍它

  1. boolean equals(Object anObject) 方法:按照字典序比较
    字典序:按照字符的大小顺序
    String重写了父类Object方法,Objec中默认使用“==”进行比较,String重写后按照如下规则比较:
public boolean equals(Object anObject) {
	// 1. 先检测this 和 anObject 是否为同一个对象比较,如果是返回true
	if (this == anObject) {
		return true;
	}
	// 2. 检测anObject是否为String类型的对象,如果是继续比较,否则返回false
	if (anObject instanceof String) {
	// 将anObject向下转型为String类型对象
		String anotherString = (String)anObject;
		int n = value.length;
	// 3. this和anObject两个字符串的长度是否相同,是继续比较,否则返回false
	if (n == anotherString.value.length) {
		char v1[] = value;
		char v2[] = anotherString.value;
		int i = 0;
		// 4. 按照字典序,从前往后逐个字符进行比较
		while (n-- != 0) {
			if (v1[i] != v2[i])
			return false;
			i++;
		}
		return true;
		}
	}
	return false;
}
public class Main {
    public static void main(String[] args) {
        int a = 1;
        int b = 1;
        int c = 2;
        String s1 = "hello";
        String s2 = "hi";
        String s3 = "hello";
        String s4 = s1;
        System.out.println(a == b);//两个值相等
        System.out.println(a == c);//两个值不相等
        System.out.println(s1 == s2);//两个字符串不一样
        System.out.println(s3 == s1);//虽然两个字符串相同,但是指向的引用不同
        System.out.println(s1 == s4);//指向同一个引用
        System.out.println(s1.equals(s3));//两个字符串相同
        System.out.println(s2.equals(s1));//两个字符串不同
    }
}

在这里插入图片描述
上述代码我们可以看到,虽然s1和s3指向的是不同的引用,但是只要字符串相同,就返回true
3. int compareTo(String anotherString) 方法:按照字典序比较
与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式:

  1. 先按照字典次序大小比较,如果出现不等的字符直接返回这两个字符的大小差值
  2. 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hi";
        String s3 = "hello";
        String s4 = "helloaaa";
        System.out.println(s1 == s2);//两个字符串不一样
        System.out.println(s3 == s1);//虽然两个字符串相同,但是指向的引用不同
        System.out.println(s1 == s4);//指向同一个引用
        System.out.println(s1.equals(s3));//两个字符串相同
        System.out.println(s2.equals(s1));//两个字符串不同
        System.out.println(s1.compareTo(s2));//返回字典序差值
        System.out.println(s1.compareTo(s3));//相同返回0
        System.out.println(s1.compareTo(s4));//前几个字符相同,返回字符串长度只差

    }
 }

在这里插入图片描述
4. int compareToIgnoreCase(String str) 方法:与compareTo不同的是忽略大小写进行比较

public class Main {
    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "hi";
        String s3 = "hello";
        String s4 = "helloaaa";
        System.out.println(s1 == s2);//两个字符串不一样
        System.out.println(s3 == s1);//虽然两个字符串相同,但是指向的引用不同
        System.out.println(s1 == s4);//指向同一个引用
        System.out.println(s1.equals(s3));//两个字符串相同
        System.out.println(s2.equals(s1));//两个字符串不同
        System.out.println(s1.compareTo(s2));//返回字典序差值
        System.out.println(s1.compareTo(s3));//相同返回0
        System.out.println(s1.compareTo(s4));//前几个字符相同,返回字符串长度只差
        System.out.println(s1.compareToIgnoreCase(s3));//忽略大小写进行比较
    }
}

在这里插入图片描述

11.1.4 字符串查找

字符串查找也是字符串中非常常见的操作,String类提供了如下查找方法:
在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        String s = "aaabbbcccddd";
        System.out.println(s.charAt(2));
        System.out.println(s.indexOf('a'));
        System.out.println(s.indexOf('a',2));
        System.out.println(s.indexOf("bb"));
        System.out.println(s.indexOf("bb",4));
        System.out.println(s.lastIndexOf('d'));
        System.out.println(s.lastIndexOf('d',10));
        System.out.println(s.lastIndexOf("dd"));
        System.out.println(s.lastIndexOf("dd",10));
    }
 }

在这里插入图片描述

11.1.5 转化

  1. 数值和字符串转化
public class Main {
    public static void main(String[] args) {
        String s1 = String.valueOf(12);
        String s2 = String.valueOf(12.12);
        String s3 = String.valueOf(true);
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        int a = Integer.parseInt(s1);
        double b = Double.parseDouble(s2);
        System.out.println(s1);
        System.out.println(s2);
    }
}

在这里插入图片描述
2. 大小写转换

public class Main {
    public static void main(String[] args) {
        String s1 = "me";
        String s2 = "YOU";
        System.out.println(s1.toUpperCase());
        System.out.println(s2.toLowerCase());
    }
}

在这里插入图片描述
注意:这里在转化的是一个新的字符串,不是在原来的字符串上修改,因为字符串具有不可变性
3. 字符串转数组和字符串转数组

public class Main {
    public static void main(String[] args) {
        String s1 = "abcdefg";
        char[] array = s1.toCharArray();
        System.out.println(Arrays.toString(array));
        String s2 = new String(array);
        System.out.println(s2);
    }
}

在这里插入图片描述
4. 格式化字符串

public class Main {
    public static void main(String[] args) {
        String s = String.format("%d-%d-%d",2024,3,11);
        System.out.println(s);
    }
}

在这里插入图片描述

11.1.6 字符串替换

使用一个指定的新的字符串替换掉已有的字符串

方法功能
String replaceAll(String regex , String replacement)替换所有指定的内容
String replaceFrist (String regex , String replacement)替换第一个指定的内容
ublic class Main {
    public static void main(String[] args) {
        String str = "hello";
        System.out.println(str.replaceAll("l","_"));
        System.out.println(str.replaceFirst("l","_"));
    }
 }

在这里插入图片描述

注意事项:字符串是一个不可变对象,替换操作并没有修改当前字符串,而是产生了一个新的字符串

11.1.7 字符串的拆分

可将一个完整的字符串按照指定的分隔符划分为若干个子串

方法功能
String[] split (String regex)将字符串按指定分隔符全部拆分
String[] split (String regex , int limit)将字符串按指定分隔符,拆分为limit组
public static void main(String[] args) {
        String str2 = "hello world hello everyone";
        String[] a = str2.split(" ");
        System.out.println(Arrays.toString(a));
        String[] b = str2.split(" ",2);
        System.out.println(Arrays.toString(b));
    }

在这里插入图片描述
字符串的拆分是常见的操作,需要重点掌握,有时分隔符不可以被直接识别,需要加上转义符号,例如:

public class Main {
    public static void main(String[] args) {
        String str = "2004.04.05";
        String[] a = str.split("\\.");
        System.out.println(Arrays.toString(a));
    }
 }

在这里插入图片描述
注意事项

  1. 字符“|”,“*”,“+”等字符都要加上转义符号,前面加上\
  2. 如果分割符号是“\”,就要写成“\\”
  3. 如果一个字符串中有多个分隔符,可以使用“|”作为连字符,下面是示例
public class Main {
    public static void main(String[] args) {
        String str = "2004-04.05"; 
        String[] a = str.split("\\.|-");
        System.out.println(Arrays.toString(a));
    }
}

在这里插入图片描述

11.1.8 字符串截取

从一个完整的字符串中截取出部分内容

方法功能
String substring(int beginIndex)从指定索引内容截取到结尾
String substring(int beginIndex, int endIndex)截取部分内容
public class Main {
    public static void main(String[] args) {
        String str = "helloworld";
        System.out.println(str.substring(2) );
        System.out.println(str.substring(2,6) );
    }
}

在这里插入图片描述
下面我们观察第二个截取的字符串,我们发现截取时候,下标左闭右开的形式,并没有截取6下标这个位置的字符

11.1.9 大小写转换

方法功能
String toUpperCase()字符串转大写
String toLowerCase()字符串转大写
public class Main {
    public static void main(String[] args) {
        String str = "helloworld";
        String str1 = str.toUpperCase();
        System.out.println(str1);
        System.out.println(str1.toLowerCase());
    }
}

在这里插入图片描述

11.1.10 字符串的不可变性

String是一种不可变的对象,是因为:

  1. 在jdk的官方文档中,给出了说明

String instance is constant. (字符串是不可变的)Overwriting this field after construction will cause problems. Additionally, it is marked with Stable to trust the contents of the array. No other facility in JDK provides this functionality (yet). Stable is safe here, because value is never null.

  1. 下面我们从源码的角度来说明
private final byte[] value;

我们看见,上述储存字符的数组被private修饰,这说明在String类之外的其他类无法访问到value,所以在类外无法对该内容进行修改,又因为String类源码为只读文档,总不能对源码进行修改,在String类中直接修改,所以字符串便有了不可变性(不可修改性)

纠正:网上有的人说,不可修改是因为有String和value均有final的修饰,这种说法是错误的,数组用final修饰只是说明该不可以引用其他对象,但是其自身是可以修改的,类用final修饰只是说明该类不想被继承
在这里插入图片描述
在这里插入图片描述
那么我们要想对字符串进行修改,可以吗?当然可以,但是我们要尽量避免,因为效率非常低下,下面我们来说明

11.1.11 字符串的修改

public class Main {
    public static void main(String[] args) {
        String s = "hello";
        System.out.println(s + " " + "world");
    }
 }

我们不推荐字符串直接这样修改的原因是因为,由于字符串的不可变性,所以在中间会创建许多临时变量,会大大降低代码的效率
在这里插入图片描述
在这里我们可以看到每次创建的时候都会new一个新对象,每次都是一个不同的对象,我们应该避免对String的直接修改,使用StringBuilder和StringBuffer大大提高效率,我们下一篇介绍~

11.2 StringBuilder, StringBuffer与OJ例题

11.2.1 StringBuilder和StringBuffer

在上一篇博客我们提到,String类具有不可变性,在每次修改字符串的时候都是创建新的对象去修改,那么现在我们引入StringBuilder和StringBuffer类. 在用这两个类建立对象的时候, 字符串就是可变的, 可用类中的一些方法对字符串进行一系列操作,这两个大类的部分功能是相同的.这里介绍一些常用的方法,要是用到其他方法,大家可参考jdk帮助手册.

方法功能
StringBuff append(String str)在尾部添加字符串,相当于String的+=
char charAt(int index)获取index位置的字符
int length()获取字符串的长度
int capacity()获取字符串在底层储存空间的大小
void ensureCapacity(int mininmumCapacity)扩容
int setCharAt(int index,char ch)将index位置的字符设置为ch
int indexOf(String str)将index位置开始查找str第一次出现的位置
int indexOf(String str,int index)从index位置开始向后查找str,返回第一次出现的位置
int lastIndexOf(String str)从最后的位置开始查找指定字符串
int lastIndexOf(String str,int index)从index位置开始查找指定字符串
StringBuff insert(int index,String str)在index位置插入:八种基本类型,String类,Object类
StringBuffer delete(int start,int end)删除[start,end)区间的字符串
StirnBuffer deleteCharAt(int index)删除index位置的字符
StringBuffer replace(int start,int end,String str)替换[start,end)区间的字符串
String substring(int start)从start位置截取字符串,直到结尾,并以String类型返回
String substring(int start, int end)将[start,end)范围字符串截取下来,并以String类型返回
StringBuffer reverse()翻转字符串
String toString()将所有字符串以String的方式返回
下面进行上述方法的演示
public class Test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("hello");
        sb1.append(" world");
        sb1.append(" !!!");//为sb1添加字符
        System.out.println(sb1);
        System.out.println(sb1.charAt(2));//获取2位置的字符
        System.out.println(sb1.length());
        sb1.setCharAt(0, 'H');//,第二个参数传入字符类型,字符用单引号引起来
        System.out.println(sb1);
        System.out.println(sb1.indexOf("ell"));//,传入字符串类型返回字符串第一次出现的位置
        System.out.println(sb1.indexOf("llo",1));//从指定位置开始查找字符串
        System.out.println(sb1.lastIndexOf("rld"));//从最后开始查找字符串
        System.out.println(sb1.lastIndexOf("wor",10));//从指定位置开始向前查找字符串
        sb1.insert(1,"uuu");//在指定位置插入指定字符串
        System.out.println(sb1);
        sb1.delete(1,4);//删除[1,4)位置的字符
        System.out.println(sb1);
        sb1.deleteCharAt(1);//删除指定位置的字符
        System.out.println(sb1);
        sb1.replace(2,5,"uuu");//把指定区间的字符串替换为指定字符串
        System.out.println(sb1);
        System.out.println(sb1.substring(4));//从指定位置开始截取字符串
        System.out.println(sb1.substring(4,7));//截取指定区间的字符串
        System.out.println(sb1.reverse());
    }
}

上述例子可以看出: String和StringBuilder最大的区别就是==String内容无法修改,而StringBuilder的内容可以修改,==频繁修改字符串考虑使用StringBuilder.

注意: String和StringBuilder类不可以直接转换,如果想要转换,可以采用如下方法:

  • String变为StringBuilder: 利用StringBuiler的构造方法,或者append方法
  • StringBuilder变为String: 利用toString方法
public class Test {
    public static void main(String[] args) {
        String s1 = "hello";
        StringBuilder sb2 = new StringBuilder(s1);//调用StringBuilder的构造方法,将String转换为StringBuilder
        StringBuilder sb3 = new StringBuilder();
        sb3.append(s1);
        System.out.println(sb2);
        System.out.println(sb3);
        String s2 = sb2.toString();//利用StringBuilder的toStirng方法,将sb转换为String
        System.out.println(s2);
    }
}

面试题

  1. String类,StringBuilder和StringBuffer的区别
  • String类的内容不可以修改,但是StringBuilder和StringBuffer可以修改
  • StringBuffer采用同步处理,属于线程安全操作,而StringBuilder未采用同步处理,属于线程不安全操作

11.2.2 String类OJ

  1. 第一个只出现一次的字符
class Solution {
    public int firstUniqChar(String s) {
        int[] count = new int [256];//定义计数数组
        for(int i = 0;i < s.length();i++){
            count[s.charAt(i)]++;//存放每个字母出现的次数
        } 
        for(int i = 0;i < s.length();i++){
            if(count[s.charAt(i)] == 1){//如果出现次数为1,则返回该下标
                return i;
            }
        }
        return -1;
    }
}
  1. 最后一个单词的长度
import java.util.Scanner;
	public class Main{
		public static void main(String[] args){
		// 循环输入
			Scanner sc = new Scanner(System.in);
			while(sc.hasNext()){
				// 获取一行单词
				String s = sc.nextLine();
				// 1. 找到最后一个空格
				// 2. 获取最后一个单词:从最后一个空格+1位置开始,一直截取到末尾
				// 3. 打印最后一个单词长度
				int len = s.substring(s.lastIndexOf(' ')+1, s.length()).length();
				System.out.println(len);
			}
		sc.close();
	}
}
  1. 判断字符串是否回文
class Solution {
	public static boolean isValidChar(char ch){
	if((ch >= 'a' && ch <= 'z') ||
	(ch >= '0' && ch <= '9')){
			return true;
		}
			return false;
	}
	public boolean isPalindrome(String s) {
	// 将大小写统一起来
	s = s.toLowerCase();
	int left = 0, right = s.length()-1;
	while(left < right){
		// 1. 从左侧找到一个有效的字符
		while(left < right && !isValidChar(s.charAt(left))){
		left++;
		}
		// 2. 从右侧找一个有效的字符
		while(left < right && !isValidChar(s.charAt(right))){
		right--;
		}
		if(s.charAt(left) != s.charAt(right)){
		return false;
		}else{
		left++;
		right--;
		}
		}
		return true;
	}
}

12. 异常

12.1 异常的概念与体系结构

12.1.1 异常的概念

在我们的生活中,一个人如果表情痛苦,我们可能会问: 你是生病了吗? 需要我陪你去看医生吗?
在这里插入图片描述
程序也和人是一样的,均会发生一些"生病"的行为,比如: 数据格式不对, 数组越界,网络中断等, 我们把这种程序出现的"生病"行为称为"异常".比如在我们之前写代码经常遇到的"报红",大多数都是异常.

  1. 算数异常
    在这里插入图片描述
  2. 数组越界异常
    在这里插入图片描述
  3. 空指针异常
    在这里插入图片描述
    从上述的展示中可以看出,异常也分不同的类型,都有与其对应的类来进行描述,总的来说,异常本质上是一种类.

12.1.2 异常的体系结构

异常的种类很多,为了对不同类型的异常或错误进行更好的管理,Java内部制定了异常的体系结构:
在这里插入图片描述

从上图中我们可以看到:

  1. Throwable是异常体系的顶层类,由它派生出了两个子类:Error和Exception,即错误和异常.
  2. Error:指的是Java虚拟机无法解决的严重问题,如JVM内部错误,资源耗尽等,典型代表就是StackOverflowError(栈溢出错误).
  3. Exception: 异常产生后程序员可以通过修改代码进行处理,可以使得程序继续执行.

12.1.3 异常的分类

根据异常发生的时间不同,我们可以将异常分为:

  1. 编译时异常
    在程序编译期间发生的异常,称为编译时异常,也称为受查异常,我们在用idea编译器时,编译器会在出现异常的语句下面划红线.
 public class Person {
        private int age;
        private String name;

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
    @Override
        protected Object clone() {
            return super.clone();
        }
}

在这里插入图片描述
从上述的代码我们可以看出,在clone方法之后我们没有对发生的异常进行声明,所以在放重写的方法那里产生了编译时异常,在clone下面划了红线.

  1. 运行时异常
    在程序执行的期间发生的异常,称为运行时异常,也称为非受查异常,RunTimeException以及子类对应的异常,都叫运行时异常,比如我们上面提到的: 算数异常(ArithmeticException),空指针异常(NullPointerException),数组越界异常(ArraryIndexOutOfBoundsException).

12.2 异常的处理

12.2.1 防御式编程

错误在代码中是客观存在的.此时就需要把出现的错误和异常及时通知程序员,主要有以下两种方式:

  1. LBYL: look before your leap. 在操作之前就进行充分的检查,即:事前防御,语法格式如下:
boolean ret = false;
ret = 操作1;
if(!ret){
	处理异常1;
	return;
}
ret = 操作2;
if(!ret){
	处理异常2;
	return;
}
......

缺点: 正常的流程和错误的处理混在一起,代码显得比较凌乱.
2. EAFP: It’s Easier to ask forgiveness than permission. 事后获取原谅比事前获取许可更容易.也就是先操作,遇到的问题放在最后一起处理,即:事后认错型.

try{
	操作1;
	操作2;
	......
}catch(异常1){
	处理异常1;
}catch(异常2){
	处理异常2;
}
.......

优势: 正常的流程和错误的处理是分开的,代码更加清晰,容易理解.
异常处理的核心思想就是EAFP
在Java中,处理异常主要有5个关键字: throw, try, catch, finally , throws.

12.2.2 异常的抛出

在编写程序时, 如果程序中出现错误, 此时就需要将错误的信息报告给调用者.
在Java中,可以使用throw关键字,抛出一个指定的异常对象,来将错误信息来报告给调用者.具体语法格式如下:

throw new xxxException ("产生异常的原因");

我们举一个例子来说明:例如我们要获取数组任意位置的元素和方法.

public static int getElement(int[] array,int index){
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }
public static void main(String[] args) {
        int[] array = {1,2,3};
        getElement(array,3);
        int[] array2 = null;
        getElement(array2,2);
    }

在这里插入图片描述
在这里插入图片描述
[注意事项]:

  1. throw必须写在方法内部
  2. 抛出的对象必须是Exception或者Exception的子类对象
  3. 如果抛出的是RunTimeException或者RunTimeException的子类,则可以不处理,直接交给jvm来处理
  4. 如果抛出的是编译时异常,用户必须处理,否者无法通过编译
  5. 异常一旦抛出,其后的代码就不会被执行

12.2.3 异常的捕获

异常的捕获,通常有两种: 异常的throws声明以及try-catch捕获处理

12.2.3.1 异常throws声明

处于方法声明的参数表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给调用者来处理,即当前方法不处理异常,提醒方法的调用者处理该异常.若调用者没有处理或throws,就会报错,语法格式如下:

修饰符号 返回值类型 方法名(参数列表)throws 异常类型1,异常类型2......{

}

下面我们拿我们上面提到的clone方法来举例

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

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
    @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
}
public class MyException {
    public static void main(String[] args)throws CloneNotSupportedException {
        Person p1 = new Person(12,"zhangsan");
        Person p2 = (Person) p1.clone();
    }
}

在这里插入图片描述
上述的代码我们可以看出如果我们在main方法调用clone方法的时候也没有直接处理异常,把异常throws了,该异常就会交给jvm来处理,若处理失败,程序会在出现异常的地方立即终止.
我们对上述代码进行一定地修正

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

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
    @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
}
public class MyException {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person p1 = new Person(12,"zhangsan");
        Person p2 = (Person) p1.clone();
    }
}

上述代码我们对Person实现了Cloneable接口,jvm处理异常成功,程序运行之后便不会报错.

[注意事项]

  1. throws必须跟在方法的参数列表之后.
  2. 声明的异常必须是Exception或者是Exception的子类.
  3. 方法内部如果有多个异常,throws之后必须跟上多个异常,之间用逗号隔开,若抛出的异常具有父子关系,直接声明父类即可.
public static int getElement(int[] array,int index) throws RuntimeException{//声明运行时异常即可
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }

  1. 调用抛出异常的方法时,必须对异常进行处理,或者继续使用throws抛出.
public static int getElement(int[] array,int index) throws RuntimeException{
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }
public static void main(String[] args) throws RuntimeException{
       int[] array = {1,2,3};
       getElement(array,3);
       int[] array2 = null;
       getElement(array2,2);
 }
12.2.3.2 捕获并处理

throws对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch.
语法格式如下:

try{
	操作1;
	操作2;
}catch(异常1){
	处理异常1;
}catch(异常2){
	处理异常2;
}finally{
    此处的代码一定会被执行
}
//后续代码

如果捕获异常成功,并且异常被处理成功,会跳出try-catch结构,之后的代码也会被执行,若有异常没有被捕获到,后续的代码就不会被执行.我们下面举个例子:

public static int getElement(int[] array,int index){
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }
    public static void main(String[] args) {
        try {
            int[] array = {1,2,3};
            getElement(array,3);
        }catch (ArrayIndexOutOfBoundsException e){
            System.out.println("处理了数组越界异常");
        }
        System.out.println("异常处理结束,后续代码被执行");
    }

关于异常的处理方式
异常的种类有很多, 我们要根据不同的业务场景来决定.
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.

[注意事项]

  1. try代码块中,如果有一个地方抛出了异常,这个地方之后的代码不会被执行.
public static int getElement(int[] array,int index) throws RuntimeException{
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }
    public static void main(String[] args) {
        try {
            int[] array = {1,2,3};
            getElement(array,3);
            int[] array2 = null;
            getElement(array2,2);
        }catch (NullPointerException e){
            System.out.println("处理了空指针异常");
        }catch (ArrayIndexOutOfBoundsException e){
            System.out.println("处理了数组越界异常");
        }
        System.out.println("异常处理结束,后续代码被执行");
    }

在这里插入图片描述
由此可见,运行结果中并,没有打印空指针异常.即可说明上述一点.
2. 如果异常类型与catch时的异常类型不匹配,异常就不会被捕获成功,也不会被处理,继续抛出,知道JVM收到后终止.

ublic static void main(String[] args) {
        try {
            int[] array = {1,2,3};
            getElement(array,3);
        }catch (NullPointerException e){
            System.out.println("处理了空指针异常");
        }
        System.out.println("异常处理结束,后续代码被执行");
    }
    public static int getElement(int[] array,int index) throws RuntimeException{
        if (array == null){
            throw new NullPointerException("element of array is null");
        }
        if (index > array.length-1||index < 0){
            throw new ArrayIndexOutOfBoundsException("index is out of array");
        }
        return array[index];
    }

在这里插入图片描述
3. try中可能会有多个异常,必须用多个catch来捕获

public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        try {
            System.out.println("before");
            arr = null;
            System.out.println(arr[100]);
            System.out.println("after");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("这是个数组下标越界异常");
            e.printStackTrace();
        } catch (NullPointerException e) {
            System.out.println("这是个空指针异常");
            e.printStackTrace();
        }
        System.out.println("after try catch");
    }

若多个异常的处理方式完全相同,也可以写成这样:

catch (ArrayIndexOutOfBoundsException|NullPointerException){
......
}
  1. 如果异常之间有父子关系,一定是子类在前,父类在后.
public static void main(String[] args) {
        try {
            int[] array = {1,2,3};
            getElement(array,3);
        }catch (Exception e){
            System.out.println("处理了异常");
        }catch (ArrayIndexOutOfBoundsException e){
            System.out.println("处理了数组越界异常");
        }
        System.out.println("异常处理结束,后续代码被执行");
    }

在这里插入图片描述
数组越界异常已经被第一个catch处理了,该异常的捕获类型属于数组越界处理的父类,而后面的子类没有处理到,所以会报错.

12.2.3.3 finally

在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收.另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的.

语法格式:
try{
	// 可能会发生异常的代码
}catch(异常类型 e){
	// 对捕获到的异常进行处理
}finally{
	// 此处的语句无论是否发生异常,都会被执行到
}
	// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public static void main(String[] args) {
        try {
            int[] array = {1,2,3};
            getElement(array,3);
        }catch (NullPointerException e){
            System.out.println("处理了空指针异常");
        }finally {
            System.out.println("一定会被执行");
        }
        System.out.println("异常处理结束,后续代码被执行");
    }

在这里插入图片描述
我们从上述执行结果可以看到,无论异常有没有被捕获到,finally一定会被执行到.
finally的代码一般用来进行一些资源清理的扫尾工作.例如:

public class TestFinally {
	public static int getData(){
		Scanner sc = null;
	try{
		sc = new Scanner(System.in);
		int data = sc.nextInt();
		return data;
	}catch (InputMismatchException e){
		e.printStackTrace();
	}finally {
		if(null != sc){
		sc.close();
	}
	return 0;
}

从上述代码中,我们知道如果sc被成功调用,直接返回了,若没有finally语句,sc的资源没有进行关闭,造成了资源泄露.

面试题

  1. throw 和 throws 的区别?throw用来抛出异常,throws用来声明异常,提醒调用者.
  2. finally中的语句一定会执行吗?一定会.

12.2.4 异常处理的流程 (总结)

  • 程序先执行 try 中的代码
  • 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
  • 如果找到匹配的异常类型, 就会执行 catch 中的代码
  • 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
  • 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
  • 如果上层调用者也没有处理的了异常, 就继续向上传递.
  • 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.

12.3 自定义异常

Java出了本身给出的异常类之外,我们还可以自定义异常,在重写的构造方法中,我们可以自定义异常原因.

ublic class PassWordException extends RuntimeException{//继承于运行时异常
    public PassWordException(String message) {//构造方法输入出现异常的原因
        super(message);
    }
}
public static void main(String[] args) {
        String password = "123457";
        if (password != "123456"){
            throw new PassWordException("password is false");
        }
    }

在这里插入图片描述
[注意事项]
继承自Exception时默认是编译时异常.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/702435.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

undetected_chromedriver驱动浏览器结束报错OSError: [WinError 6] 句柄无效

undetected_chromedriver驱动浏览器结束报错OSError: [WinError 6] 句柄无效 问题背景 使用undetected_chromedriver包驱动浏览器结束后报错句柄无效 Exception ignored in: <function Chrome.del at 0x000001DD50F07A60> Traceback (most recent call last): File “D:…

ESP32 IDF ADF 加入音频

需要把mp3制作成音频bin 用ADF自带工具 果用户需要生成自己的 audio-esp.bin&#xff0c;则需要执行 mk_audio_bin.py 脚本&#xff08;位于 $ADF_PATH/tools/audio_tone/mk_audio_tone.py&#xff09;&#xff0c;并且指定相关文件的路径。 源 MP3 文件在 tone_mp3_folder …

软考-架构设计师-综合知识总结(试卷:2009~2022)(下篇)

说明 本文档对2009到2022年试卷的综合知识进行了归纳总结&#xff0c;同时对叶宏主编的《系统架构设计师教程》划分重点。 第十七章&#xff1a;通信系统架构设计 17.2 考题总结 第十八章&#xff1a;安全架构设计 18.1 重要知识点 18.2 考题总结 第十九章&#xff1a;大数据…

2080. 区间内查询数字的频率

题目&#xff1a; 请你设计一个数据结构&#xff0c;它能求出给定子数组内一个给定值的 频率 。 子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。 请你实现 RangeFreqQuery 类&#xff1a; RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个…

跨国大文件传输需要哪些方面?怎么实现数据快速传输?

跨国大文件传输涉及到许多方面&#xff0c;包括网络速度、安全性、可靠性和法律合规性等。 以下是跨国大文件传输时需要考虑的一些重要方面&#xff1a; 高速稳定的网络连接&#xff1a;确保有足够的带宽和稳定的网络连接以支持大文件的快速传输。这可能需要考虑到跨国网络的延…

JVM 一些常见问题QA

GC Roots 虚拟机栈中引用的对象&#xff1b; 本地方法栈中JNI引用的对象&#xff1b; 方法区中类静态变量引用的对象&#xff1b; 方法区中常量引用的对象&#xff1b; Full GC是Minor GCMajor GC吗&#xff1f; Minor GC&#xff1a;回收年轻代&#xff1b; Major GC&…

[Cloud Networking] Layer 2 Protocol

文章目录 1. STP / RSTP / MSTP Protocol1.1 STP的作用1.2 STP 生成树算法的三个步骤1.3 STP缺点 2. ARP Protocol3. MACSEC 1. STP / RSTP / MSTP Protocol 1.1 STP的作用 消除二层环路&#xff1a;通过阻断冗余链路来消除网络中可能存在的环路链路备份&#xff1a;当活动链…

openh264 编码器源码分析:AnalyzePictureComplexity 函数

介绍 文件位置&#xff1a; openh264/codec/processing/src/complexityanalysis/ComplexityAnalysis.cpp 功能&#xff1a; 作为CWelsPreProcess类中一个方法&#xff0c;用来分析当前图像与参考图像之间的复杂度关系&#xff0c;以便编码策略。 原型&#xff1a; void CWels…

01背包问题(模板)

一、题目描述 描述 你有一个背包&#xff0c;最多能容纳的体积是V。 现在有n个物品&#xff0c;第i个物品的体积为vi,价值为wi。 &#xff08;1&#xff09;求这个背包至多能装多大价值的物品&#xff1f; &#xff08;2&#xff09;若背包恰好装满&#xff0c;求至多能装多大价…

XMind软件下载-详细安装教程视频

​简介 XMind是一款实用的思维导图软件&#xff0c;简单易用、美观、功能强大&#xff0c;拥有高效的可视化思维模式&#xff0c;具备可扩展、跨平台、稳定性和性能&#xff0c;真正帮助用户提高生产率&#xff0c;促进有效沟通及协作。中文官方网站&#xff1a;http://www.x…

Oracle最终会扼杀MySQL?(译)

原文网站&#xff1a;https://www.percona.com/blog/is-oracle-finally-killing-mysql/ 作者&#xff1a;Peter Zaitsev 自从Oracle收购了MySQL后&#xff0c;很多人怀疑Oracle对开源MySQL的善意&#xff0c;这篇percona的文章深入分析了Oracle已经和将要对MySQL采取的措施&a…

【C++】——继承(详解)

一 继承的定义和概念 1.1 继承的定义 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保 持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类&#xff0c;被继承的称为基类…

【多元统计】期末复习必备!按题型分类

一&#xff0c;简答题 二&#xff0c;证明题 三&#xff0c;计算题

推荐一款WPF绘图插件OxyPlot

开始 使用 NuGet 包管理器添加对 OxyPlot 的引用&#xff08;如果要使用预发布包&#xff0c;请参阅下面的详细信息&#xff09;向用户界面添加PlotView在代码中创建一个PlotModel绑定到你的属性PlotModelModelPlotView 例子 您可以在代码存储库的文件夹中找到示例。/Source/Ex…

9 - 上升的温度(高频 SQL 50 题基础版)

9 - 上升的温度 -- 找出与之前&#xff08;昨天的&#xff09;日期相比温度更高的所有日期的 id -- DATEDIFF(2007-12-31,2007-12-30); # 1 -- DATEDIFF(2010-12-30,2010-12-31); # -1select w1.id from Weather w1, Weather w2 wheredatediff(w1.recordDate,w2.recordDat…

【MySQL】表的基本增删查改(结合案例)

文章目录 1.前言2.插入数据&#xff08;Create&#xff09;2.1案例2.2单行数据全列插入2.3多行数据指定列插入2.4插入否则更新2.5替换 3. 读取数据(Retireve)3.1案例3.2全列查询3.3指定列查询3.4查询字段为表达式3.5为查询结果起别名3.6去重3.7where条件3.7.1案例 3.8排序3.9筛…

初识 AQS

一、什么是 AQS AQS是一个用来构建锁和同步器的框架。JUC 的同步器底层都是用了 AQS&#xff0c;例如ReentrantLock&#xff0c;Semaphore&#xff0c;CountDownLatch&#xff0c;CyclicBarrier&#xff0c;ReentrantReadWriteLock。 二、前置知识 在了解 AQS之前&#xff0c…

C++ 01 之 hello world

c01helloworld.cpp #include <iostream>using namespace std;int main() {cout << "hello world" << endl;return 0; } #include<iostream>; 预编译指令&#xff0c;引入头文件iostream.using namespace std; 使用标准命名空间cout <&l…

LW-DETR:实时目标检测的Transformer, Apache-2.0 开源可商用,论文实验超 YOLOv8

LW-DETR&#xff1a;实时目标检测的Transformer&#xff0c; Apache-2.0 开源可商用&#xff0c;论文实验超 YOLOv8 LW-DETR 架构实例化高效训练高效推理 目的与解法拆解ViT编码器和DETR解码器多级特征图聚合变形交叉注意力窗口注意力和全局注意力 论文&#xff1a;https://arx…

1124. 表现良好的最长时间段 (python) 前缀和 分类讨论 最大长度 力扣 面试题

给你一份工作时间表 hours&#xff0c;上面记录着某一位员工每天的工作小时数。 我们认为当员工一天中的工作小时数大于 8 小时的时候&#xff0c;那么这一天就是「劳累的一天」。 所谓「表现良好的时间段」&#xff0c;意味在这段时间内&#xff0c;「劳累的天数」是严格 大…