七万字千锤百炼,Java基础知识大总结

目录

✨写在前面

✨初识Java与开发环境的安装

Java语言概述

Java语言是什么

Java祖师爷

为什么选择Java

Java是最好的语言么

开发环境的安装

JDK的安装

IEDA的安装

✨数据类型与运算符

变量和类型

 长整型变量

 双精度浮点型变量

单精度浮点型变量

字符类型变量

字节类型变量

短整型变量

布尔类型变量

字符串类型变量

变量的作用域

 变量的命名规则

常量

 理解类型转换

理解数值提升

 运算符

算术运算符

关系运算符

逻辑运算符

 位运算符

 移位运算

条件运算符

运算符的优先级

✨程序逻辑控制

顺序结构

 分支结构

if 语句

 switch 语句

循环结构

while 循环

break

 continue

 for 循环

输入输出

输出到控制台

从键盘输入

 ✨方法的使用

方法的基本用法

什么是方法(method)

方法定义语法

方法调用的执行过程

 实参和形参的关系

没有返回值的方法

方法的重载

重载要解决的问题

使用重载

重载的规则

方法递归

递归的概念

 递归执行过程分析

 递归练习

递归小结

✨数组的定义与使用

数组的基本用法

什么是数组

创建数组

数组的使用

数组作为方法的参数

基本用法

理解引用类型

认识 null

初识 JVM 内存区域划分

数组作为方法的返回值

 二维数组

 ✨类和对象

类与对象的初步认知

 类和类的实例化

类的实例化

 类的成员

字段/属性/成员变量

方法 (method)

小结

 封装 

private实现封装

getter和setter方法

 构造方法 

基本语法

 this关键字

认识代码块

什么是代码块

普通代码块

 构造代码块 

 静态代码块

补充说明

toString方法

匿名对象

 内容重点总结

 ✨面向对象

概述

导入包中的类

静态导入

将类放到包中

基本规则

操作步骤

 包的访问权限控制

Demo1.java

常见的系统包

继承

语法规则

基本语法

protected 关键字

 更复杂的继承关系

final 关键字

组合

多态

向上转型

动态绑定

方法重写

理解多态

向下转型

super 关键字

在构造方法中调用重写的方法

总结

抽象类

抽象类的作用

接口

语法规则

实现多个接口

接口使用实例

总结


✨写在前面

为期一个多月的Java基础学习终于结束,自己也对Java有了新的认识,我认为本篇博客非常适合Java小白的入门学习,每一章节结束都有相应的习题和题解,以便于自己巩固学习。本篇博客博主制作也是非常不容易,内容干货较多,建议收藏,仔细阅读。如果你认为本篇博客写的不错的话,求点赞,求收藏,求评论,你的支持会让更多的人看到,废话不多说,让我们学期来吧!!!

✨初识Java与开发环境的安装

Java语言概述

Java语言是什么

Java是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义。不仅如此,Java还是一个有一系 列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛 应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合。

Java的开发者原本想注册Ock(橡树)商标,但是不幸被别人用过了。所以改用了一个名字——Java(一种爪哇产的咖啡),Java的开发人员非常喜欢喝这种咖啡,也寓意着为全世界献上一杯香气四溢的咖啡。因此才有了这个咖啡图标的诞生。

Java祖师爷

 詹姆斯·高斯林 (James Gosling)是一名软件专家,1955年5月19日出生于加拿大,Java编程语言的共同创始人之一,一般公认他为“Java之父”。

博士论文的题目是:"The Algebraic Manipulation of Constraints"。毕业后到IBM工作,设计IBM第一代工作站NeWS系统,但不受重视。后来转至Sun公司。1990年,与Patrick Naughton和Mike Sheridan等人合作“绿色计划”,后来发展一套语言叫做“Oak”,后改名为Java。1994年底,James Gosling在硅谷召开的“技术、教育和设计大会”上展示Java程式。2000年,Java成为世界上最流行的电脑语言。

为什么选择Java

 JavaScript和Java有什么关系呢?要说到这俩的关系,举个例子。就相当于雷锋和雷峰塔的关系或者说老婆和老婆饼的关系(没有一点关系...),最开始JavaScript叫liveScript,当时Java太火了,于是乎 liveScript更名为JavaScript借势宣传一波。
 

Java是最好的语言么

不是,因为在每个领域都有更合适的编程语言。

C 语言无疑是现代计算机软件编程语言的王者,几乎所有的操作系统都是 C 语言写成的。C++ 是面向对象的 C 语 言,一直在不断的改进。

JavaScript 是能运行在浏览器中的语言,丰富的前端界面离不开 Javascript 的功劳。近年来的 Node.js 又在后端占 有一席之地。

Python 用于系统管理,并通过高性能预编译的库,提供 API 来进行科学计算,文本处理等,是 Linux 必选的解释 性语言。

那么,Java 语言到底有什么优势可以占据排行榜第一的位置呢?

其一,语法比较简单,学过计算机编程的开发者都能快速上手。

其二,在若干了领域都有很强的竞争力,比如服务端编程,高性能网络程序,企业软件事务处理,分布式计算, Android 移动终端应用开发等等。

开发环境的安装

JDK的安装

idea是开发工具,jdk是开发环境,开发工具有很多,但是开发环境只能是jdk。

jdk的安装我会推荐你去B站或者是我下面推荐的这位博主写的安装步骤去安装,切记,一定要按照每一步的顺序安装,安装不好可能你疏忽了。

IEDA的安装

IDEA网址:https://www.jetbrains.com/idea/download/#section=windows

IDEA相关视频
IDEA安装视频:https://www.bilibili.com/video/BV1s44y1b7rV/
IDEA基础开发设置:https://www.bilibili.com/video/BV13T4y1L7qP/
IDEA基础快键键设置:https://www.bilibili.com/video/BV1Lv411i7wD/
IDEA基础调试方法:https://www.bilibili.com/video/BV1mv411y7D5/

✨数据类型与运算符

变量和类型

变量指的是程序运行时可变的量. 相当于开辟一块内存空间来保存一些数据. 类型则是对变量的种类进行了划分, 不同的类型的变量具有不同的特性

七万字千锤百炼,Java基础知识大总结_第1张图片

  变量和我们的内存紧密相关

整型变量

基本语法格式

int 变量名 = 初始值;

代码示例:

public static void main(String[] args) {
        int a = 1;
        System.out.println(a);
    }

七万字千锤百炼,Java基础知识大总结_第2张图片

注意事项:

1. int 表示变量的类型是一个整型

2. 变量名是变量的标识. 后续都是通过这个名字来使用变量

3. Java 中 = 表示赋值(和数学不一样), 意思是给变量设置一个初始值.

4. 初始化操作是可选的, 但是建议创建变量的时候都显式初始化.

5. 最后不要忘记分号, 否则会编译失败.

6. // 表示注释. 注释作为代码的解释说明部分, 不参与编译运行
 

    public static void main(String[] args) {
        //int a = 1;
        //System.out.println(a);
    }

在 Java 中, 一个 int 变量占 4 个字节. 和操作系统没有直接关系.

什么是字节?

字节是计算机中表示空间大小的基本单位. 计算机使用二进制表示数据. 我们认为 8 个二进制位(bit) 为一个字节(Byte). 我们平时的计算机为 8GB 内存, 意思是 8G 个字节. 其中 1KB = 1024 Byte, 1MB = 1024 KB, 1GB = 1024 MB. 所以 8GB 相当于 80 多亿个字节.

4 个字节表示的数据范围是 -2^31 -> 2^31-1 , 也就大概是 -21亿 到 +21亿.
 

 七万字千锤百炼,Java基础知识大总结_第3张图片

 使用以下代码查看 Java 中的整型数据范围:

 public static void main(String[] args) {
        System.out.println(Integer.MAX_VALUE);  // int 的最大值
        System.out.println(Integer.MIN_VALUE);  // int 的最小值
    }

七万字千锤百炼,Java基础知识大总结_第4张图片

 如果运算的结果超出了 int 的最大范围, 就会出现溢出的情况

 public static void main(String[] args) {
        int maxValue = Integer.MAX_VALUE;
        System.out.println(maxValue+1);
        int minValue = Integer.MIN_VALUE;
        System.out.println(minValue-1);
    }

七万字千锤百炼,Java基础知识大总结_第5张图片

 长整型变量

基本语法格式:

long 变量名 = 初始值;

代码示例:

 public static void main(String[] args) {
        long num = 10L; // 定义一个长整型变量, 初始值写作 10l 也可以(小写的 L, 不是数字1).
        System.out.println(num) ;
 
    }

注意事项:

1. 基本语法格式和创建 int 变量基本一致, 只是把类型修改成 long

2. 初始化设定的值为 10L , 表示一个长整型的数字. 10l 也可以.

3. 使用 10 初始化也可以, 10 的类型是 int, 10L 的类型是 long, 使用 10 L 或者 10 l 更好一些.

Java 中 long 类型占 8 个字节. 表示的数据范围 -2^63 -> 2^63-1

使用以下代码查看 Java 中的长整型数据范围:

  public static void main(String[] args) {
        System.out.println(Long.MAX_VALUE);
        System.out.println(Long.MIN_VALUE);
    }

七万字千锤百炼,Java基础知识大总结_第6张图片

 双精度浮点型变量

基本语法格式

double 变量名 = 初始值;

代码示例:

  public static void main(String[] args) {
        double num = 1.0;
        System.out.println(num);
    }

七万字千锤百炼,Java基础知识大总结_第7张图片

 public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a / b);
    }

七万字千锤百炼,Java基础知识大总结_第8张图片

在 Java 中, int 除以 int 的值仍然是 int(会直接舍弃小数部分).

如果想得到 0.5, 需要使用 double 类型计算

  public static void main(String[] args) {
        double a = 1.0;
        double b = 2.0;
        System.out.println(a / b);
    }

七万字千锤百炼,Java基础知识大总结_第9张图片

 public static void main(String[] args) {
            double num = 1.1;
            System.out.println(num * num);
        }

七万字千锤百炼,Java基础知识大总结_第10张图片

 Java 中的 double 虽然也是 8 个字节, 但是浮点数的内存布局和整数差别很大, 不能单纯的用 2 ^ n 的形式表示数据范围.Java 的 double 类型的内存布局遵守 IEEE 754 标准(和C语言一样), 尝试使用有限的内存空间表示可能无限的小数, 势必会存在一定的精度误差.

单精度浮点型变量

基本格式:

float 变量名 = 初始值;

代码示例:

  public static void main(String[] args) {
            float num = 1.0f;    // 写作 1.0F 也可以
            System.out.println(num);
        }

七万字千锤百炼,Java基础知识大总结_第11张图片

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

字符类型变量

基本格式:

char 变量名 = 初始值;

 代码示例:

   public static void main(String[] args) {
            char ch = 'A';
            System.out.println(ch);
        }

七万字千锤百炼,Java基础知识大总结_第12张图片

注意事项:

1. Java 中使用 单引号 + 单个字母 的形式表示字符字面值.

2. 计算机中的字符本质上是一个整数. 在 C 语言中使用 ASCII 表示字符, 而 Java 中使用 Unicode 表示字符. 因此一 个字符占用两个字节, 表示的字符种类更多, 包括中文.

   public static void main(String[] args) {
            char ch = '呵';
            System.out.println(ch);
        }

七万字千锤百炼,Java基础知识大总结_第13张图片

字节类型变量

基本语法格式:

byte 变量名 = 初始值; 

代码示例:

  public static void main(String[] args) {
            byte value = 0;
            System.out.println(value);
        }

七万字千锤百炼,Java基础知识大总结_第14张图片

注意事项:

1. 字节类型表示的也是整数. 只占一个字节, 表示范围较小 ( -128 -> +127 )

2. 字节类型和字符类型互不相干.

短整型变量

基本语法格式:

short 变量名 = 初始值; 

 代码示例:

 public static void main(String[] args) {
            short value = 0;
            System.out.println(value);
        }

七万字千锤百炼,Java基础知识大总结_第15张图片

注意事项:

1. short 占用 2 个字节, 表示的数据范围是 -32768 -> +32767

2. 这个表示范围比较小, 一般不推荐使用.

布尔类型变量

基本语法格式:

boolean 变量名 = 初始值;

代码示例:

 public static void main(String[] args) {
            boolean value = true;
            System.out.println(value);
        }

七万字千锤百炼,Java基础知识大总结_第16张图片

注意事项: 

1. boolean 类型的变量只有两种取值, true 表示真, false 表示假.

2. Java 的 boolean 类型和 int 不能相互转换, 不存在 1 表示 true, 0 表示 false 这样的用法.

3. boolean 类型有些 JVM 的实现是占 1 个字节, 有些是占 1 个比特位。
 

七万字千锤百炼,Java基础知识大总结_第17张图片

 Test.java:4: 错误: 二元运算符 '+' 的操作数类型错误 System.out.println(value + 1);

字符串类型变量

基本语法格式:

String 变量名 = "初始值"; 

代码示例:

  public static void main(String[] args) {
            String name = "zhangsan";
            System.out.println(name);
        }

七万字千锤百炼,Java基础知识大总结_第18张图片

注意事项:

1. Java 使用 双引号 + 若干字符 的方式表示字符串字面值.

2. 和上面的类型不同, String 不是基本类型, 而是引用类型(后面重点解释).

3. 字符串中的一些特定的不太方便直接表示的字符需要进行转义

字符串的 + 操作, 表示字符串拼接:

public static void main(String[] args) {
            String a = "hello";
            String b = "world";
            String c = a + b;
            System.out.println(c);
        }

七万字千锤百炼,Java基础知识大总结_第19张图片

 还可以用字符串和整数进行拼接:

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

七万字千锤百炼,Java基础知识大总结_第20张图片

因此我们可以很方便的使用 System.out.println 同时打印多个字符串或数字

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

七万字千锤百炼,Java基础知识大总结_第21张图片

变量的作用域

也就是该变量能生效的范围, 一般是变量定义所在的代码块

七万字千锤百炼,Java基础知识大总结_第22张图片

 变量的命名规则

硬性指标:

1. 一个变量名只能包含数字, 字母, 下划线

2. 数字不能开头.

3. 变量名是大小写敏感的. 即 num 和 Num 是两个不同的变量

软性指标:

1. 变量命名要具有描述性, 见名知意.

2. 变量名不宜使用拼音(但是不绝对).

3. 变量名的词性推荐使用名词.

4. 变量命名推荐 小驼峰命名法, 当一个变量名由多个单词构成的时候, 除了第一个单词之外, 其他单词首字母都大写.

小驼峰命名示例:

int maxValue = 100; 
String studentName = "张三";

常量

字面值常量

10 // int 字面值常量(十进制) 
010 // int 字面值常量(八进制) 由数字 0 开头. 010 也就是十进制的 8 
0x10 // int 字面值常量(十六进制) 由数字 0x 开头. 0x10 也就是十进制的 16 
10L // long 字面值常量. 也可以写作 10l (小写的L) 
1.0 // double 字面值常量. 也可以写作 1.0d 或者 1.0D 
1.5e2 // double 字面值常量. 科学计数法表示. 相当于 1.5 * 10^2 
1.0f // float 字面值常量, 也可以写作 1.0F 
true // boolen 字面值常量, 同样的还有 false 
'a' // char 字面值常量, 单引号中只能有一个字符
"abc" // String 字面值常量, 双引号中可以有多个字符.

final 关键字修饰的常量

七万字千锤百炼,Java基础知识大总结_第23张图片

 理解类型转换

七万字千锤百炼,Java基础知识大总结_第24张图片

long 表示的范围更大, 可以将 int 赋值给 long, 但是不能将 long 赋值给 int.

double 表示的范围更大, 可以将 int 赋值给 double, 但是不能将 double 赋值给 int.

结论: 不同数字类型的变量之间赋值, 表示范围更小的类型能隐式转换成范围较大的类型, 反之则不行

int 和 boolean 相互赋值

七万字千锤百炼,Java基础知识大总结_第25张图片

结论: int 和 boolean 是毫不相干的两种类型, 不能相互赋值

int字面值常量 给 byte 赋值

七万字千锤百炼,Java基础知识大总结_第26张图片

注意: byte 表示的数据范围是 -128 -> +127, 256 已经超过范围, 而 100 还在范围之内.

结论: 使用字面值常量赋值的时候, Java 会自动进行一些检查校验, 判定赋值是否合理.

使用强制类型转换

  public static void main(String[] args) {
            int a = 0;
            double b = 10.5;
            a = (int)b;
            System.out.println(a);
        }

七万字千锤百炼,Java基础知识大总结_第27张图片

 七万字千锤百炼,Java基础知识大总结_第28张图片

结论: 使用 (类型) 的方式可以将 double 类型强制转成 int.

1. 强制类型转换可能会导致精度丢失. 如刚才的例子中, 赋值之后, 10.5 就变成 10 了, 小数点后面的部分被忽略.

2. 强制类型转换不是一定能成功, 互不相干的类型之间无法强转

类型转换小结

1. 不同数字类型的变量之间赋值, 表示范围更小的类型能隐式转换成范围较大的类型.

2. 如果需要把范围大的类型赋值给范围小的, 需要强制类型转换, 但是可能精度丢失.

3. 将一个字面值常量进行赋值的时候, Java 会自动针对数字范围进行检查.

理解数值提升

int 和 long 混合运算

public static void main(String[] args) {
            int a = 10;
            long b = 20;
            int c = a + b; // 编译出错, 提示将 long 转成 int 会丢失精度
            long d = a + b; // 编译通过
        }

结论:

当 int 和 long 混合运算的时候, int 会提升成 long, 得到的结果仍然是 long 类型, 需要使用 long 类型的变量来 接收结果.

如果非要用 int 来接收结果, 就需要使用强制类型转换.

byte 和 byte 的运算

   public static void main(String[] args) {
            byte a = 10;
            byte b = 20;
            byte c = a + b;
            System.out.println(c);
 
            // 编译报错Test.java:5: 错误: 不兼容的类型: 从int转换到byte可能会有损失
        }

结论:

byte 和 byte 都是相同类型, 但是出现编译报错. 原因是, 虽然 a 和 b 都是 byte, 但是计算 a + b 会先将 a 和 b 都 提升成 int, 再进行计算, 得到的结果也是 int, 这是赋给 c, 就会出现上述错误.

正确的写法:

public static void main(String[] args) {
            byte a = 10;
            byte b = 20;
            byte c = (byte)(a + b);
            System.out.println(c);
        }

七万字千锤百炼,Java基础知识大总结_第29张图片

类型提升小结:

1. 不同类型的数据混合运算, 范围小的会提升成范围大的.

2. 对于 short, byte 这种比 4 个字节小的类型, 会先提升成 4 个字节的 int , 再运算.

int 和 String 之间的相互转换

int 转成 String

    public static void main(String[] args) {
            int num = 10;
            // 方法1
            String str1 = num + "";
            // 方法2
            String str2 = String.valueOf(num);
            System.out.println(str1);
            System.out.println(str2);
        }

七万字千锤百炼,Java基础知识大总结_第30张图片

String 转成 int

   public static void main(String[] args) {
            String str = "100";
            int num = Integer.parseInt(str);
            System.out.println(str);
        }

七万字千锤百炼,Java基础知识大总结_第31张图片

 运算符

算术运算符

基本四则运算符

 + - * / %

a)int / int 结果还是 int, 需要使用 double 来计算

 public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a / b);
    }

七万字千锤百炼,Java基础知识大总结_第32张图片

 b)0 不能作为除数

   public static void main(String[] args) {
        int a = 1;
        int b = 0;
        System.out.println(a / b);
    }  

七万字千锤百炼,Java基础知识大总结_第33张图片

 c) % 表示取余, 不仅仅可以对 int 求模, 也能对 double 来求模

System.out.println(11.5 % 2.0);

七万字千锤百炼,Java基础知识大总结_第34张图片

增量赋值运算符 += -= *= /= %=

public static void main(String[] args) {
        int a = 10;
        a += 1; // 等价于 a = a + 1
        System.out.println(a);
    }

七万字千锤百炼,Java基础知识大总结_第35张图片

 自增/自减运算符 ++ --

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

七万字千锤百炼,Java基础知识大总结_第36张图片

结论:

1. 如果不取自增运算的表达式的返回值, 则前置自增和后置自增没有区别.

2. 如果取表达式的返回值, 则前置自增的返回值是自增之后的值, 后置自增的返回值是自增之前的值.

关系运算符

== != < > =
public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println(a == b);
        System.out.println(a != b);
        System.out.println(a < b);
        System.out.println(a > b);
        System.out.println(a <= b);
        System.out.println(a >= b);
    }

七万字千锤百炼,Java基础知识大总结_第37张图片

注意: 关系运算符的表达式返回值都是boolean型

逻辑运算符

&& || !

注意: 逻辑运算符的操作数(操作数往往是关系运算符的结果)和返回值都是 boolean

逻辑与 &&

规则: 两个操作数都为 true, 结果为 true, 否则结果为 false.

 public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = 30;
        System.out.println(a < b && b < c);
    }

七万字千锤百炼,Java基础知识大总结_第38张图片

逻辑或 ||

规则: 两个操作数都为 false, 结果为 false, 否则结果为 true

 public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = 30;
        System.out.println(a < b || b < c);
    }

七万字千锤百炼,Java基础知识大总结_第39张图片

逻辑非 !

规则: 操作数为 true, 结果为 false; 操作数为 false, 结果为 true(这是个单目运算符, 只有一个操作数)

  public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println(!(a < b));
    }

七万字千锤百炼,Java基础知识大总结_第40张图片

 短路求值

&& 和 || 遵守短路求值的规则.

1. 对于 && , 如果左侧表达式值为 false, 则表达式的整体的值一定是 false, 无需计算右侧表达式.

2. 对于 ||, 如果左侧表达式值为 true, 则表达式的整体的值一定是 true, 无需计算右侧表达式.

  public static void main(String[] args) {
        //我们都知道, 计算 10 / 0 会导致程序抛出异常. 
        //但是代码却能正常运行, 说明 10 / 0 并没有真正被求值.
        System.out.println(10 > 20 && 10 / 0 == 0); // 打印 false
        System.out.println(10 < 20 || 10 / 0 == 0); // 打印 true
    }

七万字千锤百炼,Java基础知识大总结_第41张图片

 位运算符

& | ~ ^

七万字千锤百炼,Java基础知识大总结_第42张图片

 按位与 &: 如果两个二进制位都是 1, 则结果为 1, 否则结果为 0

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

七万字千锤百炼,Java基础知识大总结_第43张图片

 按位或 |: 如果两个二进制位都是 0, 则结果为 0, 否则结果为 1

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

七万字千锤百炼,Java基础知识大总结_第44张图片

 注意: 当 & 和 | 的操作数为整数(int, short, long, byte) 的时候, 表示按位运算, 当操作数为 boolean 的时候, 表示逻辑运算.

按位取反 ~: 如果该位为 0 则转为 1, 如果该位为 1 则转为 0

  public static void main(String[] args) {
        int a = 0xf;
        System.out.printf("%x\n", ~a);
 
    }

七万字千锤百炼,Java基础知识大总结_第45张图片

注意:

1. 0x 前缀的数字为 十六进制 数字. 十六进制可以看成是二进制的简化表示方式. 一个十六进制数字对应 4 个二进 制位.

2. 0xf 表示 10 进制的 15, 也就是二进制的 1111

3. printf 能够格式化输出内容, %x 表示按照十六进制输出.

4. \n 表示换行符

按位异或 ^: 如果两个数字的二进制位相同, 则结果为 0, 相异则结果为 1

 public static void main(String[] args) {
        int a = 0x1;
        int b = 0x2;
        System.out.printf("%x\n", a ^ b);
    }

七万字千锤百炼,Java基础知识大总结_第46张图片

 移位运算

<< >> 

七万字千锤百炼,Java基础知识大总结_第47张图片

 左移 <<: 最左侧位不要了, 最右侧补 0.

 public static void main(String[] args) {
        int a = 0x10;
        System.out.printf("%x\n", a << 1);
    }

七万字千锤百炼,Java基础知识大总结_第48张图片

 右移 >>: 最右侧位不要了, 最左侧补符号位 (正数补0, 负数补1)

  public static void main(String[] args) {
        int a = 0x10;
        System.out.printf("%x\n", a >> 1);
        // 运行结果(注意, 是按十六进制打印的)
        //8
        int b = 0xffff0000;
        System.out.printf("%x\n", b >> 1);
        // 运行结果(注意, 是按十六进制打印的)
        //ffff8000
    }

七万字千锤百炼,Java基础知识大总结_第49张图片

条件运算符

表达式1 ? 表达式2 : 表达式3

当表达式1 的值为 true 时, 整个表达式的值为表达式2 的值; 当表达式1 的值为 false 时, 整个表达式的值为 表达式 3 的值.

 public static void main(String[] args) {
        // 求两个整数的最大值
        int a = 10;
        int b = 20;
        int max = a > b ? a : b;
        System.out.println(max);
    }

七万字千锤百炼,Java基础知识大总结_第50张图片

运算符的优先级

public static void main(String[] args) {
        System.out.println(1 + 2 * 3);
    }

七万字千锤百炼,Java基础知识大总结_第51张图片

 public static void main(String[] args) {
        System.out.println(10 < 20 && 20 < 30);
 
    }

七万字千锤百炼,Java基础知识大总结_第52张图片

 此时明显是先计算的 10 < 20 和 20 < 30, 再计算 &&. 否则 20 && 20 这样的操作是语法上有误的(&& 的操作数只能是 boolean).

小结

1. % 操作再 Java 中也能针对 double 来计算.

2. 需要区分清楚 前置自增 和 后置自增之间的区别.

3. 由于 Java 是强类型语言, 因此对于类型检查较严格, 因此像 && 之类的运算操作数必须是 boolean.

4. 要区分清楚 & 和 | 什么时候是表示按位运算, 什么时候表示逻辑运算

✨程序逻辑控制

顺序结构

顺序结构比较简单. 像我们之前写过的代码就是顺序结构的, 按照代码书写的顺序一行一行执行。

public static void main(String[] args) {
        System.out.println("aaa");
        System.out.println("bbb");
        System.out.println("ccc");
    }

 如果调整代码的书写顺序, 则执行顺序也发生变化

 public static void main(String[] args) {
        System.out.println("aaa");
        System.out.println("ccc");
        System.out.println("bbb");
    }

 分支结构

if 语句

基本语法形式1

if(布尔表达式){
    //条件满足时执行代码
}

基本语法形式2

if(布尔表达式){
    //条件满足时执行代码
}else{
    //条件不满足时执行代码
}

基本语法形式3 

if(布尔表达式){
    //条件满足时执行代码
}else if(布尔表达式){
    //条件满足时执行代码
}else{
    //条件都不满足时执行代码
}

代码示例1: 判定一个数字是奇数还是偶数

public static void main(String[] args) {
        int num = 10;
        if (num % 2 == 0) {
            System.out.println("num 是偶数");
        } else {
            System.out.println("num 是奇数");
        }
    }

代码示例2: 判定一个数字是正数还是负数

 public static void main(String[] args) {
        int num = 10;
        if (num > 0) {
            System.out.println("num 是正数");
        } else if (num < 0) {
            System.out.println("num 是负数");
        } else {
            System.out.println("num 是 0");
        }
    }
 
  

代码示例3: 判定某一年份是否是闰年

 public static void main(String[] args) {
        int year = 2000;
        if (year % 100 == 0) {
            // 判定世纪闰年
            if (year % 400 == 0) {
                System.out.println("是闰年");
            } else {
                System.out.println("不是闰年");
            }
        } else {
            // 普通闰年
            if (year % 4 == 0) {
                System.out.println("是闰年");
            } else {
                System.out.println("不是闰年");
            }
        }
}

 悬垂 else 问题

   public static void main(String[] args) {
        int x = 10;
        int y = 10;
        if (x == 10)
            if (y == 10)
                System.out.println("aaa");
            else
                System.out.println("bbb");
    }
//书写代码时,尽量在if和else加括号

 switch 语句

基本语法

switch(整数|枚举|字符|字符串){
 case 内容1 : {
 内容满足时执行语句;
 [break;]
 }
 case 内容2 : {
 内容满足时执行语句;
 [break;]
 }
 ...
 default:{
 内容都不满足时执行语句;
 [break;]
 } 
}

代码示例: 根据 day 的值输出星期

  public static void main(String[] args) {
        int day = 1;
        switch(day) {
            case 1:
                System.out.println("星期一");
                break;
            case 2:
                System.out.println("星期二");
                break;
            case 3:
                System.out.println("星期三");
                break;
            case 4:
                System.out.println("星期四");
                break;
            case 5:
                System.out.println("星期五");
                break;
            case 6:
                System.out.println("星期六");
                break;
            case 7:
                System.out.println("星期日");
                break;
            default:
                System.out.println("输入有误");
                break;
        }
    }

根据 switch 中值的不同, 会执行对应的 case 语句. 遇到 break 就会结束该 case 语句. 如果 switch 中的值没有匹配的 case, 就会执行 default 中的语句. 我们建议一个 switch 语句最好都要带上 default。

注意事项1 break 不要遗漏, 否则会失去 "多分支选择" 的效果

public static void main(String[] args) {
        int day = 1;
        switch(day) {
            case 1:
                System.out.println("星期一");
                // break;
            case 2:
                System.out.println("星期二");
                break;
        }
 
    }

 注意事项2 switch 中的值只能是 整数|枚举|字符|字符串

   public static void main(String[] args) {
        double num = 1.0;
        switch(num) {
            case 1.0:
                System.out.println("hehe");
                break;
            case 2.0:
                System.out.println("haha");
                break;
        }
    }

注意事项3 switch 不能表达复杂的条件

/ 例如: 如果 num 的值在 10 到 20 之间, 就打印 hehe
// 这样的代码使用 if 很容易表达, 但是使用 switch 就无法表示. 
if (num > 10 && num < 20) {
 System.out.println("hehe");
}

循环结构


while 循环

基本语法格式:

while(循环条件){ 
循环语句; 
}

代码示例1: 打印 1 - 10 的数字

 public static void main(String[] args) {
        int num = 1;
        while (num <= 10) {
            System.out.print(" "+num);
            num++;
        }
    }

代码示例2: 计算 1 - 100 的和

  public static void main(String[] args) {
        int n = 1;
        int result = 0;
        while (n <= 100) {
            result += n;
            n++;
        }
        System.out.println(result);
    }

代码示例3: 计算 5 的阶乘

  public static void main(String[] args) {
        int n = 1;
        int result = 1;
        while (n <= 5) {
            result *= n;
            n++;
        }
        System.out.println(result);
    }

代码示例4: 计算 1! + 2! + 3! + 4! + 5!

public static void main(String[] args) {
        int num = 1;
        int sum = 0;
       // 外层循环负责求阶乘的和
        while (num <= 5) {
            int factorResult = 1;
            int tmp = 1;
            // 里层循环负责完成求阶乘的细节.
            while (tmp <= num) {
                factorResult *= tmp;
                tmp++;
            }
            sum += factorResult;
            num++;
        }
        System.out.println("sum = " + sum);
    }

注意事项

1. 和 if 类似, while 下面的语句可以不写 { } , 但是不写的时候只能支持一条语句. 建议还是加上 { }

2. 和 if 类似, while 后面的 { 建议和 while 写在同一行.

3. 和 if 类似, while 后面不要多写 分号, 否则可能导致循环不能正确执行.
 

int num = 1; 
while (num <= 10); { 
 System.out.println(num); 
 num++; 
} 
// 执行结果
[无任何输出, 程序死循环]

break

break 的功能是让循环提前结束。

代码示例: 找到 100 - 200 中第一个 3 的倍数

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

 continue

continue 的功能是跳过这次循环, 立即进入下次循环

代码示例: 找到 100 - 200 中所有3 的倍数

  public static void main(String[] args) {
        int num = 100;
        while (num <= 200) {
            if (num % 3 != 0) {
                num++; // 这里的 ++ 不要忘记! 否则会死循环.
                continue;
            }
            System.out.println("找到了 3 的倍数, 为:" + num);
            num++;
        }
    }

 for 循环

基本语法

for(表达式1;表达式2;表达式3){ 
循环体; 
} 

表达式1: 用于初始化循环变量.

表达式2: 循环条件

表达式3: 更新循环变量

代码示例1: 打印 1 - 10 的数字

for (int i = 1; i <= 10; i++) { 
 System.out.println(i); 
} 

代码示例2: 计算 1 - 100 的和

int sum = 0; 
for (int i = 1; i <= 100; i++) { 
 sum += i; 
} 
System.out.println("sum = " + sum); 
// 执行结果
5050 

代码示例3: 计算 5 的阶乘

int result = 0; 
for (int i = 1; i <= 5; i++) { 
 result *= i; 
} 
System.out.println("result = " + result); 

注意事项 (和while循环类似)

1. 和 if 类似, for 下面的语句可以不写 { } , 但是不写的时候只能支持一条语句. 建议还是加上 { }

2. 和 if 类似, for 后面的 { 建议和 while 写在同一行.

3. 和 if 类似, for 后面不要多写 分号, 否则可能导致循环不能正确执行.
 

输入输出

输出到控制台

基本语法

  public static void main(String[] args) {
        System.out.println("msg"); // 输出一个字符串, 带换行
        System.out.print("msg"); // 输出一个字符串, 不带换行
        System.out.printf("format, msg"); // 格式化输出
    }

println 输出的内容自带 \n,

print 不带 \n printf 的格式化输出方式和 C 语言的 printf 是基本一致的

格式化字符串

从键盘输入

使用 Scanner 读取字符串/整数/浮点数

import java.util.Scanner; // 需要导入 util 包
public class fsfa {
    public static void main(String[] args) {
        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 循环读取 N 个数字

import java.util.Scanner; // 需要导入 util 包
public class fsfa {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        double sum = 0.0;
        int num = 0;
        while (sc.hasNextDouble()) {
            double tmp = sc.nextDouble();
            sum += tmp;
            num++;
        }
        System.out.println("sum = " + sum);
        System.out.println("avg = " + sum / num);
        sc.close();
    }
}

 ✨方法的使用

方法的基本用法

什么是方法(method)

方法存在的意义(不要背, 重在体会):

1. 是能够模块化的组织代码(当代码规模比较复杂的时候).

2. 做到代码被重复使用, 一份代码可以在多个位置使用.

3. 让代码更好理解更简单.

4. 直接调用现有方法开发, 不必重复造轮子

回忆一个之前写过的代码: 计算 1! + 2! + 3! + 4! + 5!

int sum = 0;
for (int i = 1; i <= 5; i++) {
    int tmp = 1;
    for (int j = 1; j <= i; j++) {
        tmp *= j;
   }
    sum += tmp;
}
System.out.println("sum = " + sum);

方法定义语法

基本语法

// 方法定义
public static 方法返回值 方法名称([参数类型 形参 ...]){
 方法体代码;
 [return 返回值];
}
// 方法调用
返回值变量 = 方法名称(实参...);

代码示例: 实现一个方法实现两个整数相加


        public static void main(String[] args) {
            int a = 10;
            int b = 20;
 
            // 方法的调用
            int ret = add(a, b);
            System.out.println("ret = " + ret);
        }
        // 方法的定义
        public static int add(int x, int y) {
            return x + y;
        }

七万字千锤百炼,Java基础知识大总结_第53张图片

注意事项

1. public 和 static 两个关键字在此处具有特定含义, 我们暂时不讨论, 后面会详细介绍.

2. 方法定义时, 参数可以没有. 每个参数要指定类型

3. 方法定义时, 返回值也可以没有, 如果没有返回值, 则返回值类型应写成 void

4. 方法定义时的参数称为 "形参", 方法调用时的参数称为 "实参".

5. 方法的定义必须在类之中, 代码书写在调用位置的上方或者下方均可.

6. Java 中没有 "函数声明" 这样的概念.

方法调用的执行过程

基本规则

定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.

当方法被调用的时候, 会将实参赋值给形参.

参数传递完毕后, 就会执行到方法体代码.

当方法执行完毕之后(遇到 return 语句), 就执行完毕, 回到方法调用位置继续往下执行

一个方法可以被多次调用.

代码示例1 计算两个整数相加

public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println("第一次调用方法之前");
        int ret = add(a, b);
        System.out.println("第一次调用方法之后");
        System.out.println("ret = " + ret);
        System.out.println("第二次调用方法之前");
        ret = add(30, 50);
        System.out.println("第二次调用方法之后");
        System.out.println("ret = " + ret);
    }
    public static int add(int x, int y) {
        System.out.println("调用方法中 x = " + x + " y = " + y);
        return x + y;
    }

代码示例: 计算 1! + 2! + 3! + 4! + 5!

public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 5; i++) {
            sum += factor(i);
        }
        System.out.println("sum = " + sum);
    }
    public static int factor(int n) {
        System.out.println("计算 n 的阶乘中! n = " + n);
        int result = 1;
        for (int i = 1; i <= n; i++) {
            result *= i;
        }
        return result;
    }

以N!为例画图讲解

public static int fac(int n){
        if(n==1){
            return 1;
        }
        int tmp = n * fac(n-1);
        return tmp;
    }
 
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int y = fac(n);
        System.out.println(y);
    }

七万字千锤百炼,Java基础知识大总结_第54张图片

 实参和形参的关系

代码示例: 交换两个整型变量

public static void main(String[] args) {
        int a = 10;
        int b = 20;
        swap(a, b);
        System.out.println("a = " + a + " b = " + b);
    }
    public static void swap(int x, int y) {
        int tmp = x;
        x = y;
        y = tmp;
    }

 原因分析

刚才的代码, 没有完成数据的交换.

对于基础类型来说, 形参相当于实参的拷贝. 即传值调用

int a = 10;
int b = 20;
int x = a;
int y = b;
int tmp = x;
x = y;
y = tmp;

可以看到, 对 x 和 y 的修改, 不影响 a 和 b.

解决办法: 传引用类型参数 (例如数组来解决这个问题)

public static void main(String[] args) {
        int[] arr = {10, 20};
        swap(arr);
        System.out.println("a = " + arr[0] + " b = " + arr[1]);
    }
    public static void swap(int[] arr) {
        int tmp = arr[0];
        arr[0] = arr[1];
        arr[1] = tmp;
    }

没有返回值的方法

方法的返回值是可选的. 有些时候可以没有的

代码示例

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

方法的重载

重载要解决的问题

代码示例

class Test {
 public static void main(String[] args) {
 int a = 10;
 int b = 20;
 int ret = add(a, b);
 System.out.println("ret = " + ret);
 double a2 = 10.5;
 double b2 = 20.5;
 double ret2 = add(a2, b2);
 System.out.println("ret2 = " + ret2);
 }
 public static int add(int x, int y) {
 return x + y;
 }
}
// 编译出错
//Test.java:13: 错误: 不兼容的类型: 从double转换到int可能会有损失
//              double ret2 = add(a2, b2);

那么是不是应该创建这样的代码呢?

class Test {
 public static void main(String[] args) {
 int a = 10;
 int b = 20;
 int ret = addInt(a, b);
 System.out.println("ret = " + ret);
 double a2 = 10.5;
 double b2 = 20.5;
 double ret2 = addDouble(a2, b2);
 System.out.println("ret2 = " + ret2);
 }
 public static int addInt(int x, int y) {
 return x + y;
 }
 public static double addDouble(double x, double y) {
 return x + y;
 }
}

这样的写法是对的(例如 Go 语言就是这么做的), 但是 Java 认为 addInt 这样的名字不友好, 不如直接就叫 add。

使用重载

代码示例

public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int ret = add(a, b);
        System.out.println("ret = " + ret);
        double a2 = 10.5;
        double b2 = 20.5;
        double ret2 = add(a2, b2);
        System.out.println("ret2 = " + ret2);
        double a3 = 10.5;
        double b3 = 10.5;
        double c3 = 20.5;
        double ret3 = add(a3, b3, c3);
        System.out.println("ret3 = " + ret3);
    }
       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;
    }

七万字千锤百炼,Java基础知识大总结_第55张图片

 方法的名字都叫 add. 但是有的 add 是计算 int 相加, 有的是 double 相加; 有的计算两个数字相加, 有的是计算三个数 字相加. 同一个方法名字, 提供不同版本的实现, 称为 方法重载。

重载的规则

针对同一个类也可以是不同类:

方法名相同

方法的参数不同(参数个数或者参数类型)

方法的返回值类型不影响重载.

代码示例

class Test {
 public static void main(String[] args) {
 int a = 10;
 int b = 20;
 int ret = add(a, b);
 System.out.println("ret = " + ret);
 }
 public static int add(int x, int y) {
 return x + y;
 }
 public static double add(int x, int y) {
 return x + y;
 }
}
// 编译出错
//Test.java:13: 错误: 已在类 Test中定义了方法 add(int,int)

方法递归

递归的概念

一个方法在执行过程中调用自身, 就称为 "递归". 递归相当于数学上的 "数学归纳法", 有一个起始条件, 然后有一个递推公式.

例如, 我们求 N! 起始条件: N = 1 的时候, N! 为 1. 这个起始条件相当于递归的结束条件. 递归公式: 求 N! , 直接不好求, 可以把问题转换成 N! => N * (N-1)!

代码示例: 递归求 N 的阶乘

public static int fac(int n){
        if(n==1){
            return 1;
        }
        int tmp = n * fac(n-1);
        return tmp;
    }
 
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int y = fac(n);
        System.out.println(y);
    }

七万字千锤百炼,Java基础知识大总结_第56张图片

 递归执行过程分析

  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;
    }

七万字千锤百炼,Java基础知识大总结_第57张图片

 递归练习

代码示例1 按顺序打印一个数字的每一位(例如 1234 打印出 1 2 3 4)

public static void print(int num) {
    if (num > 9) {
        print(num / 10);
   }
    System.out.println(num % 10);
}

代码示例2 递归求 1 + 2 + 3 + ... + 10

public static int sum(int num) { 
 if (num == 1) { 
 return 1; 
 } 
 return num + sum(num - 1); 
} 

代码示例3 写一个递归方法,输入一个非负整数,返回组成它的数字之和. 例如,输入 1729, 则应该返回1+7+2+9, 它的和是19

public static int sum(int num) { 
 if (num < 10) { 
 return num; 
 } 
 return num % 10 + sum(num / 10); 
} 

代码示例4 求斐波那契数列的第 N 项

public static int fib(int n) { 
 if (n == 1 || n == 2) { 
 return 1; 
 } 
 return fib(n - 1) + fib(n - 2); 
} 
 //循环(迭代)实现
    public static int fib2(int n) {
        if(n == 1 || n == 2) {
            return 1;
        }
        int f1 = 1;
        int f2 = 1;
        int f3 = 0;
        for (int i = 3; i <= n; i++) {
            f3 = f1+f2;
            f1 = f2;
            f2 = f3;
        }
        return f3;
    } 

递归小结

递归是一种重要的编程解决问题的方式.

有些问题天然就是使用递归方式定义的(例如斐波那契数列, 二叉树等), 此时使用递归来解就很容易.

有些问题使用递归和使用非递归(循环)都可以解决. 那么此时更推荐使用循环, 相比于递归, 非递归程序更加高效

✨数组的定义与使用

数组的基本用法

什么是数组

数组本质上就是让我们能 "批量" 创建相同类型的变量

例如:

如果需要表示两个数据, 那么直接创建两个变量即可 int a; int b

如果需要表示五个数据, 那么可以创建五个变量 int a1; int a2; int a3; int a4; int a5;

但是如果需要表示一万个数据, 那么就不能创建一万个变量了. 这时候就需要使用数组, 帮我们批量创建.

注意事项: 在 Java 中, 数组中包含的变量必须是 相同类型

创建数组

基本语法

// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };
int[] arr = new int[]{1, 2, 3};
int[] arr = {1, 2, 3};

七万字千锤百炼,Java基础知识大总结_第58张图片

 注意事项: 静态初始化的时候, 数组元素个数和初始化数据的格式是一致的.

数组的使用

代码示例: 获取长度 & 访问元素

 public static void main3(String[] args) {
        int[] array = {1,2,3,4,5,6};
        //System.out.println(array.length);
        System.out.println(array[3]);
        array[3] = 19;
        System.out.println(array[3]);
    }

七万字千锤百炼,Java基础知识大总结_第59张图片

 int[] arr = {1, 2, 3};
        // 获取数组长度
        System.out.println("length: " + arr.length); // 执行结果: 3
        // 访问数组中的元素
        System.out.println(arr[1]); // 执行结果: 2
        System.out.println(arr[0]); // 执行结果: 1
        arr[2] = 100;
        System.out.println(arr[2]); // 执行结果: 100

注意事项

1. 使用 arr.length 能够获取到数组的长度. . 这个操作为成员访问操作符. 后面在面向对象中会经常用到.

2. 使用 [ ] 按下标取数组元素. 需要注意, 下标从 0 开始计数

3. 使用 [ ] 操作既能读取数据, 也能修改数据.

4. 下标访问操作不能超出有效范围 [0, length - 1] , 如果超出有效范围, 会出现下标越界异常
 

代码示例: 下标越界

public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        System.out.println(arr[100]);
 
    }

七万字千锤百炼,Java基础知识大总结_第60张图片

代码示例: 遍历数组

 public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

七万字千锤百炼,Java基础知识大总结_第61张图片

 代码示例: 使用 for-each 遍历数组

public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        for (int x : arr) {
            System.out.println(x);
        }
 
    }

七万字千锤百炼,Java基础知识大总结_第62张图片

数组作为方法的参数

基本用法

代码示例: 打印数组内容

public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        printArray(arr);
    }
    public static void printArray(int[] a) {
        for (int x : a) {
            System.out.println(x);
        }
    }

七万字千锤百炼,Java基础知识大总结_第63张图片

理解引用类型

代码示例1 参数传数组类型

  public static void printf(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5,6};
        printf(array);
    }

 七万字千锤百炼,Java基础知识大总结_第64张图片

 代码示例2 参数传内置类型

public static void func1(int[] array) {
        array = new int[]{11,2,13,4,51,61};
    }
    public static void func(int[] array) {
        array[0] = 899;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5,6};
        System.out.println(Arrays.toString(array));
        func1(array);
        System.out.println(Arrays.toString(array));
    }

七万字千锤百炼,Java基础知识大总结_第65张图片

public static void func1(int[] array) {
        array = new int[]{11,2,13,4,51,61};
    }
    public static void func2(int[] array) {
        array[0] = 899;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5,6};
        System.out.println(Arrays.toString(array));
        func2(array);
        System.out.println(Arrays.toString(array));
    }

七万字千锤百炼,Java基础知识大总结_第66张图片

 七万字千锤百炼,Java基础知识大总结_第67张图片

 

 总结: 所谓的 "引用" 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实 只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).

认识 null

null 在 Java 中表示 "空引用" , 也就是一个无效的引用.

public static void main(String[] args) {
        int[] arr = null;
        System.out.println(arr[0]);
    }

七万字千锤百炼,Java基础知识大总结_第68张图片

 null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操 作. 一旦尝试读写, 就会抛出 NullPointerException.

初识 JVM 内存区域划分

一个宿舍楼会划分成几个不同的区域: 大一学生, 大二学生... 计算机专业学生, 通信专业学生.... 内存也是类似, 这个大走廊被分成很多部分, 每个区域存放不同的数据.

程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址.

虚拟机栈(JVM Stack): 重点是存储局部变量表(当然也有其他信息). 我们刚才创建的 int[] arr 这样的存储地 址的引用就是在这里保存.

本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局 部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的. 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3} )

方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数 据. 方法编译出的的字节码就是保存在这个区域.

运行时常量池(Runtime Constant Pool): 是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意 从 JDK 1.7 开始, 运行时常量池在堆上).

Native 方法:

JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数进行和操 作系统底层进行一些交互. 因此在 Java 开发中也会调用到一些 C++ 实现的函数.

这里的 Native 方法就是指这些 C++ 实现的, 再由 Java 来调用的函数

数组作为方法的返回值

代码示例: 写一个方法, 将数组中的每个元素都 * 2

public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        transform(arr);
        printArray(arr);
    }
    public static void printArray(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
    public static void transform(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            arr[i] = arr[i] * 2;
        }
    }

七万字千锤百炼,Java基础知识大总结_第69张图片

 七万字千锤百炼,Java基础知识大总结_第70张图片

 二维数组

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

 基本语法

 数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
  public static void main5(String[] args) {
        int[][] array = {
    {1,2,3},{4,5,6}};
        int[][] array2 = new int[][]{
    {1,2,3},{4,5,6}};
        int[][] array3 = new int[2][3];
    }
 public static void main(String[] args) {
        int[][] array = {
    {1,2,3},{4,5,6}};
        /*System.out.println(array.length);
        System.out.println(array[0].length);*/
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array[i].length; j++) {
                System.out.print(array[i][j] +" ");
            }
            System.out.println();
        }
        System.out.println("================");
        for (int[] ret : array) {
            for (int x :ret) {
                System.out.print(x +" ");
            }
            System.out.println();
        }
        System.out.println("================");
        System.out.println(Arrays.deepToString(array));
    }

七万字千锤百炼,Java基础知识大总结_第71张图片

 七万字千锤百炼,Java基础知识大总结_第72张图片

七万字千锤百炼,Java基础知识大总结_第73张图片

 ✨类和对象

类与对象的初步认知

七万字千锤百炼,Java基础知识大总结_第74张图片

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

JAVA是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

面向过程注重的是过程,在整个过程中所涉及的行为,就是功能。

面向对象注重的是对象,也就是参与过程所涉及到的主体。是通过逻辑将一个个功能实现连接起来。

面向过程: 1.把冰箱打开 2. 把大象放入 3. 冰箱关起来 面向对象: 打开冰箱,储存,关闭都是对冰箱的操作,是冰 箱的行为。冰箱就是一个对象,所以只要操作冰箱所具备的功能,都要定义在冰箱中。

【面向对象概念】

1.面向对象是思考问题的一种思考方式,是一种思想。比如:概念与实例。理论与实践。名和实等等。

2.类就是一类对象的统称。对象就是这一类具体化的一个实例。

3.面向对象的好处:将复杂的事情变简单了,只要面对一个对象就行。

【面向对象设计】

面向对象设计把握一个重要的经验:谁拥有数据,谁对外提供操作这些数据(私有)的方法!(被动的一方是数据的拥有者,主动的一方是执行者)

开发时:找对象,建对象,用对象,并维护对象之间的关系。

面向过程

七万字千锤百炼,Java基础知识大总结_第75张图片

 面向对象

七万字千锤百炼,Java基础知识大总结_第76张图片

 类和类的实例化

类就是一类对象的统称。对象就是这一类具体化的一个实例。

简单的例子:我们做月饼的模子就是一个类,而通过这个模子可以做出月饼,那么在这个例子当中,类就是那个模 子,而月饼就是那个对象,所以月饼就是一个实体。一个模子可以实例化无数个对象。

总的来说:类相当于一个模板,对象是由模板产生的样本。一个类,可以产生无数的对象。

声明一个类就是创建一个新的数据类型,而类在 Java 中属于引用类型, Java 使用关键字 class 来声明类。我们来 看以下简单的声明一个类。

基本语法

// 创建类
class {  
    field;//成员属性
    method;//成员方法
}
// 实例化对象
 <对象名> = new ();

class为定义类的关键字,ClassName为类的名字,{}中为类的主体。

类中的元素称为:成员属性。类中的函数称为:成员方法。

示例:

class Person {
    public int age;//成员属性 实例变量
    public String name;
    public String sex;
    public void eat() {//成员方法
       System.out.println("吃饭!");  
   }
    public void sleep() {
       System.out.println("睡觉!");  
   }
}

类的实例化

用类类型创建对象的过程,称为类的实例化

类的实例化 用类类型创建对象的过程,称为类的实例化

1. 类只是一个模型一样的东西,限定了类有哪些成员。

2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。

3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

七万字千锤百炼,Java基础知识大总结_第77张图片

class Person {
    public int age;//成员属性 实例变量
    public String name;
    public String sex;
    public void eat() {//成员方法
        System.out.println("吃饭!");
    }
    public void sleep() {
        System.out.println("睡觉!");
    }
}
   class Main{
    public static void main(String[] args) {
        Person person = new Person();//通过new实例化对象
        person.eat();//成员方法调用需要通过对象的引用调用
        person.sleep();
        //产生对象     实例化对象
        Person person2 = new Person();
        Person person3 = new Person();
    }
}

七万字千锤百炼,Java基础知识大总结_第78张图片

注意事项

new 关键字用于创建一个对象的实例.

使用 . 来访问对象中的属性和方法.

同一个类可以创建对个实例

class Person {
    //普通的成员变量  都是属于对象的
    private String name;
    private int age=19;
    //静态成员变量->类变量
    public static int count;//0
}

七万字千锤百炼,Java基础知识大总结_第79张图片

 类的成员

字段/属性/成员变量

在类中, 但是方法外部定义的变量. 这样的变量我们称为 "字段" 或 "属性" 或 "成员变量"(三种称呼都可以, 一般不会严格区分)

用于描述一个类中包含哪些数据.

class Person {
    public String name;   // 字段
    public int age;
}
 
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);
        System.out.println(person.age);
    }
}

七万字千锤百炼,Java基础知识大总结_第80张图片

使用 . 访问对象的字段.

"访问" 既包含读, 也包含写.

对于一个对象的字段如果没有显式设置初始值, 那么会被设置一个默认的初值

默认值规则

对于各种数字类型, 默认值为 0.

对于 boolean 类型, 默认值为 false.

对于引用类型(String, Array, 以及自定制类), 默认值为 null

认识 null

null 在 Java 中为 "空引用", 表示不引用任何对象. 类似于 C 语言中的空指针. 如果对 null 进行 . 操作就会引发异常.

class Person {
    public String name;
    public int age;
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name.length());   // 获取字符串长度
    }
}

七万字千锤百炼,Java基础知识大总结_第81张图片

 字段就地初始化

很多时候我们不希望字段使用默认值, 而是需要我们显式设定初值. 可以这样写:

class Person {
    public String name = "张三";
    public int age = 18;
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);
        System.out.println(person.age);
    }
}

七万字千锤百炼,Java基础知识大总结_第82张图片

方法 (method)

用于描述一个对象的行为

class Person {
    public int age = 18;
    public String name = "张三";
 
    public void show() {
        System.out.println("我叫" + name + ", 今年" + age + "岁");
    }
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.show();
    }
}

七万字千锤百炼,Java基础知识大总结_第83张图片

 方法中还有一种特殊的方法称为 构造方法 (construction method) 在实例化对象的时候会被自动调用到的方法, 方法名字和类名相同, 用于对象的初始化. 虽然我们前面已经能将属性就地初始化, 但是有些时候可能需要进行一些更复杂的初始化逻辑, 那么就可以使用构造方法。

static 关键字

1,修饰属性

2,修饰方法

3,代码块

4,修饰类

a)修饰属性

Java静态属性和类相关, 和具体的实例无关. 换句话说, 同一个类的不同实例共用同一个静态属性.

class TestDemo{
    public int a;
    public static int count;
}
class Main{
 
    public static void main(String[] args) {
        TestDemo t1 = new TestDemo();
        t1.a++;
        TestDemo.count++;
        System.out.println(t1.a);
        System.out.println(TestDemo.count);
        System.out.println("============");
        TestDemo t2 = new TestDemo();
        t2.a++;
        TestDemo.count++;
        System.out.println(t2.a);
        System.out.println(TestDemo.count);
    }
}

七万字千锤百炼,Java基础知识大总结_第84张图片

  count被static所修饰,所有类共享。且不属于对象,访问方式为:类名 . 属性。

七万字千锤百炼,Java基础知识大总结_第85张图片

 b) 修饰方法

如果在任何方法上应用 static 关键字,此方法称为静态方法。

静态方法属于类,而不属于类的对象。

可以直接调用静态方法,而无需创建类的实例。

静态方法可以访问静态数据成员,并可以更改静态数据成员的值。

class TestDemo{
    public int a;
    public static int count;
    public static void change() {
        count = 100;
        //a = 10; error 不可以访问非静态数据成员
    }
}
 class Main{
    public static void main(String[] args) {
        TestDemo.change();//无需创建实例对象 就可以调用
        System.out.println(TestDemo.count);
    }
}

七万字千锤百炼,Java基础知识大总结_第86张图片

注意事项1:

静态方法和实例无关, 而是和类相关. 因此这导致了两个情况:

静态方法不能直接使用非静态数据成员或调用非静态方法(非静态数据成员和方法都是和实例相关的).

this和super两个关键字不能在静态上下文中使用(this 是当前实例的引用, super是当前实例父类实例的引用, 也 是和当前实例相关).
 

注意事项2:

我们曾经写的方法为了简单, 都统一加上了 static. 但实际上一个方法具体要不要带 static, 都需要是情形而定. main 方法为 static 方法

小结

class Person {
    public int age;//实例变量   存放在对象内
    public String name;//实例变量
    public String sex;//实例变量
    public static int count;//类变量也叫静态变量,编译时已经产生,属于类本身,且只有一份。存放在方法区
    public final int SIZE = 10;//被final修饰的叫常量,也属于对象。 被final修饰,后续不可更改
    public static final int  COUNT = 99;//静态的常量,属于类本身,只有一份 被final修饰,后续不可更改
    //实例成员函数
    public void eat() {
        int a = 10;//局部变量
        System.out.println("eat()!");
    }
    //实例成员函数
    public void sleep() {
        System.out.println("sleep()!");
    }
    //静态成员函数
    public static void staticTest(){
        //不能访问非静态成员
        //sex = "man"; error
        System.out.println("StaticTest()");
    }
}
 class Main{
    public static void main(String[] args) {
        //产生对象 实例化对象
        Person person = new Person();//person为对象的引用
        System.out.println(person.age);//默认值为0
        System.out.println(person.name);//默认值为null
        //System.out.println(person.count);//会有警告!
        //正确访问方式:
        System.out.println(Person.count);
        System.out.println(Person.COUNT);
        Person.staticTest();
        //总结:所有被static所修饰的方法或者属性,全部不依赖于对象。
        person.eat();
        person.sleep();
    }
}

七万字千锤百炼,Java基础知识大总结_第87张图片

 七万字千锤百炼,Java基础知识大总结_第88张图片

 封装 

什么叫封装?

> 开篇就在讨论一个问题: 软件开发的本质就是对程序复杂程度的管理. 如果一个软件代码复杂程 度太高, 那么就无法继续维护. 如何管理复杂程度? 封装就是最基本的方法.

在我们写代码的时候经常会涉及两种角色: 类的实现者和类的调用者.

封装的本质就是让类的调用者不必太多的了解类的实现者是如何实现类的, 只要知道如何使用类就行了.

这样就降低了类使用者的学习和使用成本, 从而降低了复杂程度.

private实现封装

private/ public 这两个关键字表示 "访问权限控制" .

被public 修饰的成员变量或者成员方法, 可以直接被类的调用者使用.

被private 修饰的成员变量或者成员方法, 不能被类的调用者使用

 换句话说, 类的使用者根本不需要知道, 也不需要关注一个类都有哪些 private 的成员. 从而让类调用者以更低的成本来使用类.

直接使用 public

class Person {
    public String name = "张三";
    public int age = 18;
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println("我叫" + person.name + ", 今年" + person.age + "岁");
    }
}

这样的代码导致类的使用者(main方法的代码)必须要了解 Person 类内部的实现, 才能够使用这个类. 学习成本较高一旦类的实现者修改了代码(例如把 name 改成 myName), 那么类的使用者就需要大规模的修改自己的代码, 维护成本较高.

class Person {
    private String name = "张三";
    private int age = 18;
 
    public void show() {
        System.out.println("我叫" + name + ", 今年" + age + "岁");
    }
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.show();
    }
} 

七万字千锤百炼,Java基础知识大总结_第89张图片

此时字段已经使用 private 来修饰. 类的调用者(main方法中)不能直接使用. 而需要借助 show 方法. 此时类的使 用者就不必了解 Person 类的实现细节.

同时如果类的实现者修改了字段的名字, 类的调用者不需要做出任何修改(类的调用者根本访问不到 name, age 这样的字段).

注意事项

private 不光能修饰字段, 也能修饰方法 通常情况下我们会把字段设为 private 属性, 但是方法是否需要设为 public, 就需要视具体情形而定. 一般我们希 望一个类只提供 "必要的" public 方法, 而不应该是把所有的方法都无脑设为 public。

getter和setter方法

当我们使用 private 来修饰字段的时候, 就无法直接使用这个字段了.

代码示例

class Person {
    private String name = "张三";
    private int age = 18;
 
    public void show() {
        System.out.println("我叫" + name + ", 今年" + age + "岁");
    }
}
class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.age = 20;
        person.show();
    }
} 

七万字千锤百炼,Java基础知识大总结_第90张图片

 代码示例

class Person {
    private String name;//实例成员变量
    private int age;
 
    public void setName(String name){
        //name = name;//不能这样写
        this.name = name;//this引用,表示调用该方法的对象
    }
    public String getName(){
        return name;
    }
 
    public void show(){
        System.out.println("name: "+name+" age: "+age);
    }
 
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("caocao");
        String name = person.getName();
        System.out.println(name);
        person.show();
    }
}

七万字千锤百炼,Java基础知识大总结_第91张图片

注意事项

getName 即为 getter 方法, 表示获取这个成员的值.

setName 即为 setter 方法, 表示设置这个成员的值.

当set方法的形参名字和类中的成员属性的名字一样的时候,如果不使用this, 相当于自赋值. this 表示当前实例 的引用.

不是所有的字段都一定要提供 setter / getter 方法, 而是要根据实际情况决定提供哪种方法. 在 IDEA 中可以使用 alt + insert (或者 alt + F12) 快速生成 setter / getter 方法.

在 VSCode 中可以使用鼠标右键 菜单 -> 源代码操作 中自动生成 setter / getter 方法
 

 构造方法 

基本语法

构造方法是一种特殊方法, 使用关键字new实例化新对象时会被自动调用, 用于完成初始化操作.

new 执行过程

为对象分配内存空间

调用对象的构造方法

语法规则

1.方法名称必须与类名称相同

2.构造方法没有返回值类型声明

3.每一个类中一定至少存在一个构造方法(没有明确定义,则系统自动生成一个无参构造)

注意事项

如果类中没有提供任何的构造函数,那么编译器会默认生成一个不带有参数的构造函数

若类中定义了构造方法,则默认的无参构造将不再生成.

构造方法支持重载. 规则和普通方法的重载一致

代码示例

class Person {
 
    private String name;//实例成员变量
    private int age;
    private String sex;
    //默认构造函数 构造对象
    public Person() {
        this.name = "caocao";
        this.age = 10;
        this.sex = "男";
    }
    //带有3个参数的构造函数
    public Person(String name,int age,String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void show(){
        System.out.println("name: "+name+" age: "+age+" sex: "+sex);
    }
 
}
 class Main{
    public static void main(String[] args) {
        Person p1 = new Person();//调用不带参数的构造函数 如果程序没有提供会调用不带参数的构造函数
        p1.show();
        Person p2 = new Person("zhangfei",80,"男");//调用带有3个参数的构造函数
        p2.show();
    }
}

七万字千锤百炼,Java基础知识大总结_第92张图片

 this关键字

this表示当前对象引用(注意不是当前对象). 可以借助 this 来访问对象的字段和方法

class Person {
    private String name;//实例成员变量
    private int age;
    private String sex;
 
    //默认构造函数 构造对象
    public Person() {
        //this调用构造函数
        this("bit", 12, "man");//必须放在第一行进行显示
    }
 
    //这两个构造函数之间的关系为重载。
    public Person(String name,int age,String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    public void show() {
        System.out.println("name: "+name+" age: "+age+" sex: "+sex);
    }
}
 class Main {
    public static void main(String[] args) {
        Person person = new Person();//调用不带参数的构造函数
        person.show();
    }
}

七万字千锤百炼,Java基础知识大总结_第93张图片

 我们会发现在构造函数的内部,我们可以使用this关键字,构造函数是用来构造对象的,对象还没有构造好, 我们就使用了this,那this还代表当前对象吗?当然不是,this代表的是当前对象的引用。

认识代码块

什么是代码块

字段的初始化方式有:

1. 就地初始化

2. 使用构造方法初始化

3. 使用代码块初始化

普通代码块

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

七万字千锤百炼,Java基础知识大总结_第94张图片

 构造代码块 

class Person{
    private String name;//实例成员变量
    private int age;
    private String sex;
    public Person() {
        System.out.println("I am Person 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);
    }
}
 class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.show();
    }
}

七万字千锤百炼,Java基础知识大总结_第95张图片

 静态代码块

class Person{
    private String name;//实例成员变量
    private int age;
    private String sex;
    private static int count = 0;//静态成员变量 由类共享数据 方法区
    public Person(){
        System.out.println("I am Person init()!");
    }
    //实例代码块
    {
        this.name = "bit";
        this.age = 12;
        this.sex = "man";
        System.out.println("I am instance init()!");
    }
    //静态代码块
    static {
        count = 10;//只能访问静态数据成员
        System.out.println("I am static init()!");
    }
    public void show(){
        System.out.println("name: "+name+" age: "+age+" sex: "+sex);
    }
}
 class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person();//静态代码块是否还会被执行?
    }
}

七万字千锤百炼,Java基础知识大总结_第96张图片

注意事项

静态代码块不管生成多少个对象,其只会执行一次,且是最先执行的。

静态代码块执行完毕后, 实例代码块(构造块)执行,再然后是构造函数执行。

补充说明

toString方法

class Person {
    private String name;
    private int age;
    public Person(String name,int age) {
        this.age = age;
        this.name = name;
    }
    public void show() {
        System.out.println("name:"+name+" " + "age:"+age);
    }
}
 class Main {
    public static void main(String[] args) {
        Person person = new Person("caocao",19);
        person.show();
        //我们发现这里打印的是一个地址的哈希值 原因:调用的是Object的toString方法
        System.out.println(person);
    }
}

七万字千锤百炼,Java基础知识大总结_第97张图片

 可以使用 toString 这样的方法来将对象自动转成字符串

class Person {
    private String name;
    private int age;
    public Person(String name,int age) {
        this.age = age;
        this.name = name;
    }
    public void show() {
        System.out.println("name:"+name+" " + "age:"+age);
    }
    //重写Object的toString方法
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
 class Main {
    public static void main(String[] args) {
        Person person = new Person("caocao",19);
        person.show();
        System.out.println(person);
    }
}

 七万字千锤百炼,Java基础知识大总结_第98张图片

注意事项:

toString 方法会在 println 的时候被自动调用.

将对象转成字符串这样的操作我们称为 序列化.

toString 是 Object 类提供的方法, 我们自己创建的 Person 类默认继承自 Object 类, 可以重写 toString 方法实现我们自己版本的转换字符串方法. (关于继承和重写这样的概念, 我们后面会重点介绍)

@Override 在 Java 中称为 "注解", 此处的 @Override 表示下面实现的 toString 方法是重写了父类的方法. 关于 注解后面的课程会详细介绍

匿名对象

没有引用的对象称为匿名对象.

匿名对象只能在创建对象时使用.

如果一个对象只是用一次, 后面不需要用了, 可以考虑使用匿名对象

class Person {
    private String name;
    private int age;
    public Person(String name,int age) {
        this.age = age;
        this.name = name;
    }
    public void show() {
        System.out.println("name:"+name+" " + "age:"+age);
    }
}
class Main {
    public static void main(String[] args) {
        new Person("caocao",19).show();//通过匿名对象调用方法
    }
}

七万字千锤百炼,Java基础知识大总结_第99张图片

 内容重点总结

一个类可以产生无数的对象,类就是模板,对象就是具体的实例。

类中定义的属性,大概分为几类:类属性,对象属性。其中被static所修饰的数据属性称为类属性, static修饰的方法称为类方法,特点是不依赖于对象,我们只需要通过类名就可以调用其属性或者方法。

静态代码块优先实例代码块执行,实例代码块优先构造函数执行。

this关键字代表的是当前对象的引用。并不是当前对象。

 ✨面向对象

概述

包 (package) 是组织类的一种方式.

使用包的主要目的是保证类的唯一性.

例如, 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致 代码不能编译通过.

导入包中的类

Java 中已经提供了很多现成的类供我们使用. 例如

public class TextDemo {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类.但是这种写法比较麻烦一些, 可以使用 import 语句导入包

import java.util.Date;
    public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

七万字千锤百炼,Java基础知识大总结_第100张图片

  如果需要使用 java.util 中的其他类, 可以使用 import java.util.*

import java.util.*;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
        // 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
   }
}

但是我们更建议显式的指定要导入的类名. 否则还是容易出现冲突的情况

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        // util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
        Date date = new Date();
        System.out.println(date.getTime());
    }
}

七万字千锤百炼,Java基础知识大总结_第101张图片

  在这种情况下需要使用完整的类名

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        System.out.println(date.getTime());
   }
}

注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要. import 只是为了写代码的时候更方便. import 更类似于 C++ 的 namespace 和 using

静态导入

使用 import static 可以导入包中的静态的方法和字段

import static java.lang.System.*;
public class Test {
    public static void main(String[] args) {
        out.println("hello");
   }
}

使用这种方式可以更方便的写一些代码, 例如

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
        // 静态导入的方式写起来更方便一些. 
        // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
   }
}

将类放到包中

基本规则

在文件的最上方加上一个 package 语句指定该代码在哪个包中.

包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).

包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存 储代码.

如果一个类没有 package 语句, 则该类被放到一个默认包中.

操作步骤

1) 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包

七万字千锤百炼,Java基础知识大总结_第102张图片

2) 在弹出的对话框中输入包名, 例如 demo1

七万字千锤百炼,Java基础知识大总结_第103张图片

3) 在包中创建类, 右键包名 -> 新建 -> 类, 然后输入类名即可

七万字千锤百炼,Java基础知识大总结_第104张图片

 4) 同时我们也看到了, 在新创建的 Test.java 文件的最上方, 就出现了一个 package 语句

七万字千锤百炼,Java基础知识大总结_第105张图片

 包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用. 如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使 用.

下面的代码给了一个示例. Demo1 和 Demo2 是同一个包中, Test 是其他包中.

Demo1.java

package com.bit.demo;
public class Demo1 {
    int value = 0;
}

Demo2.java

package com.bit.demo; 
public class Demo2 { 
    public static void main(String[] args) { 
    Demo1 demo = new Demo1(); 
    System.out.println(demo.value); 
 } 
} 

七万字千锤百炼,Java基础知识大总结_第106张图片

 Test.java

package com.bit.demo; 
public class Demo2 { 
   public static void main(String[] args) { 
   Demo1 demo = new Demo1(); 
   System.out.println(demo.value); 
 } 
} 

 // 编译出错 Error:(6, 32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问

常见的系统包

1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。

2. java.lang.reflect:java 反射编程包;

3. java.net:进行网络编程开发包。

4. java.sql:进行数据库开发的支持包。

5. java.util:是java提供的工具程序包。(集合类等) 非常重要

6. java.io:I/O编程开发包。

继承

背景

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法). 有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.

例如, 设计一个类表示动物

注意, 我们可以给每个类创建一个单独的 java 文件. 类名必须和 .java 文件名匹配(大小写敏感).

// Animal.java 
public class Animal { 
   public String name; 
   public Animal(String name) { 
   this.name = name; 
 } 
 
   public void eat(String food) { 
   System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Cat.java 
class Cat { 
   public String name; 
   public Cat(String name) { 
   this.name = name; 
 } 
 
   public void eat(String food) { 
   System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
class Bird { 
   public String name; 
   public Bird(String name) { 
   this.name = name; 
 } 
 
   public void eat(String food) { 
   System.out.println(this.name + "正在吃" + food); 
 } 
 
   public void fly() { 
   System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 

这个代码我们发现其中存在了大量的冗余代码.

仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系:

这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.

这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.

从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义).

此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类 和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果.

语法规则

基本语法

class 子类 extends 父类 { 
 
} 

使用 extends 指定父类.

Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).

子类会继承父类的所有 public 的字段和方法.

对于父类的 private 的字段和方法, 子类中是无法访问的.

子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用

对于上面的代码, 可以使用继承进行改进. 此时我们让 Cat 和 Bird 继承自 Animal 类, 那么 Cat 在定义的时候就不必再 写 name 字段和 eat 方法.

七万字千锤百炼,Java基础知识大总结_第107张图片

class Bird extends Animal { 
   public Bird(String name) { 
   super(name); 
 } 
   public void fly() { 
   System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 
public class Test { 
   public static void main(String[] args) { 
   Cat cat = new Cat("小黑"); 
   cat.eat("猫粮"); 
   Bird bird = new Bird("圆圆"); 
   bird.fly(); 
  } 
} 

如果我们把 name 改成 private, 那么此时子类就不能访问了.

class Bird extends Animal { 
   public Bird(String name){ 
   super(name); 
 } 
   public void fly() { 
   System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 
// 编译出错
Error:(19, 32) java: name 在 Animal 中是 private 访问控制

protected 关键字

刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 "封装" 的初衷. 两全其美的办法就是 protected 关键字

对于类的调用者来说, protected 修饰的字段和方法是不能访问的

对于类的子类和同一个包的其他类 来说, protected修饰的字段和方法是可以访问的

// Animal.java 
public class Animal { 
   protected String name; 
   public Animal(String name) {
   this.name = name; 
 } 
   public void eat(String food) { 
   System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
public class Bird extends Animal { 
   public Bird(String name){ 
   super(name); 
 } 
   public void fly() { 
 // 对于父类的 protected 字段, 子类可以正确访问
   System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 
// Test.java 和 Animal.java 不在同一个包之中了. 
public class Test { 
   public static void main(String[] args) { 
   Animal animal = new Animal("小动物"); 
   System.out.println(animal.name); // 此时编译出错, 无法访问 name 
 } 
}

小结: Java 中对于字段和方法共有四种访问权限

private: 类内部能访问, 类外部不能访问

默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.

protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.

public : 类内部和类的调用者都能访问
七万字千锤百炼,Java基础知识大总结_第108张图片

 更复杂的继承关系

// Animal.java 
public Animal { 
 ... 
} 
// Cat.java 
public Cat extends Animal { 
 ... 
} 
// ChineseGardenCat.java 
public ChineseGardenCat extends Cat { 
 ... 
} 
// OrangeCat.java 
public Orange extends ChineseGardenCat { 
 ... 
} 
...... 

如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类

时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一 系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加 复杂.

但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了. 如果想从语法上进行限制继承, 就可以使用final 关键字

final 关键字

曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改).

final int a = 10; 
a = 20; // 编译出错

final 关键字也能修饰类, 此时表示被修饰的类就不能被继承.

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
} 
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承

final 关键字的功能是 限制 类被继承 "限制" 这件事情意味着 "不灵活".

在编程中, 灵活往往不见得是一件好事. 灵活可能意味着更容易出错.

是用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果. 例如表示一个学校:

public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
} 

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段. 这是我们设计类的一种常用方式之一

组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 "包含" 若干学生和教师

继承表示 is - a 语义

在上面的 "动物和猫" 的例子中, 我们可以理解成一只猫也 "是" 一种动物

多态

向上转型

在刚才的例子中, 我们写了形如下面的代码

Bird bird = new Bird("圆圆");

这个代码也可以写成这个样子

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型

为啥叫 "向上转型"?

在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表 示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 "向上转型" , 表示往父类的方向转.
七万字千锤百炼,Java基础知识大总结_第109张图片

方法传参

public class Test { 
   public static void main(String[] args) { 
   Bird bird = new Bird("圆圆"); 
   feed(bird); 
 } 
   public static void feed(Animal animal) { 
   animal.eat("谷子"); 
 } 
} 
 
// 执行结果
圆圆正在吃谷子

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例

方法返回

public class Test { 
   public static void main(String[] args) { 
   Animal animal = findMyAnimal(); 
 } 
   public static Animal findMyAnimal() { 
   Bird bird = new Bird("圆圆"); 
   return bird; 
 } 
} 

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

public class Test { 
   public static void main(String[] args) { 
   Animal animal = findMyAnimal(); 
 } 
   public static Animal findMyAnimal() { 
   Bird bird = new Bird("圆圆"); 
   return bird; 
 } 
} 

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例

动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢? 对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志

// Animal.java 
public class Animal { 
    protected String name; 
    public Animal(String name) { 
    this.name = name; 
 } 
    public void eat(String food) { 
    System.out.println("我是一只小动物"); 
    System.out.println(this.name + "正在吃" + food); 
 }
}
// Bird.java 
public class Bird extends Animal { 
   public Bird(String name) { 
   super(name); 
 } 
   public void eat(String food) { 
   System.out.println("我是一只小鸟"); 
   System.out.println(this.name + "正在吃" + food); 
 } 
}
// Test.java 
public class Test { 
   public static void main(String[] args) { 
   Animal animal1 = new Animal("圆圆"); 
   animal1.eat("谷子"); 
   Animal animal2 = new Bird("扁扁"); 
   animal2.eat("谷子"); 
 } 
} 

// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子

此时, 我们发现:

animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向 Bird 类型的实例.

针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而 animal2.eat() 实际调用了子类的方法

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引 用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写

针对刚才的 eat 方法来说: 子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).

关于重写的注意事项

1. 重写和重载完全不一样. 

2. 普通方法可以重写, static 修饰的静态方法不能重写

3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.

4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

方法权限示例: 将子类的 eat 改成 private

// Animal.java 
public class Animal { 
 public void eat(String food) { 
 ... 
 } 
} 
// Bird.java 
public class Bird extends Animal { 
 // 将子类的 eat 改成 private 
 private void eat(String food) { 
 ... 
 } 
} 
// 编译出错
//Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
//eat(java.lang.String) 
// 正在尝试分配更低的访问权限; 以前为public

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定

// Bird.java 
public class Bird extends Animal { 
 @Override 
 private void eat(String food) { 
 ... 
 } 
} 

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发 现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写. 我们推荐在代码中进行重写方法时显式加上 @Override 注解

小结: 重载和重写的区别

七万字千锤百炼,Java基础知识大总结_第110张图片

 事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述的是相同的事情, 只是侧重点不同

理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了. 我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

代码示例: 打印多种形状

class Shape { 
 public void draw() { 
 // 啥都不用干
 } 
}
class Cycle extends Shape { 
  @Override 
  public void draw() { 
  System.out.println("○"); 
 } 
} 
class Rect extends Shape { 
  @Override 
  public void draw() { 
  System.out.println("□"); 
 } 
} 
class Flower extends Shape { 
   @Override 
   public void draw() { 
   System.out.println("♣"); 
 } 
} 
// Test.java 
public class Test { 
   public static void main(String[] args) { 
   Shape shape1 = new Flower(); 
   Shape shape2 = new Cycle(); 
   Shape shape3 = new Rect(); 
   drawMap(shape1); 
   drawMap(shape2); 
   drawMap(shape3); 
 }
// 打印单个图形
 public static void drawShape(Shape shape) { 
 shape.draw(); 
 } 
} 

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当 前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现 (和 shape 对应的实例相关), 这种行为就称为 多态

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低

封装是让类的调用者不需要知道类的实现细节.

多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低

2) 能够降低代码的 "圈复杂度", 避免使用大量的 if - else

public static void drawShapes() { 
    Rect rect = new Rect(); 
    Cycle cycle = new Cycle(); 
    Flower flower = new Flower(); 
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
 
 for(String shape : shapes) { 
 if(shape.equals("cycle")) { 
    cycle.draw(); 
 } else if (shape.equals("rect")) { 
    rect.draw(); 
 } else if (shape.equals("flower")) { 
    flower.draw(); 
 } 
 } 
} 

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单

public static void drawShapes() { 
   // 我们创建了一个 Shape 对象的数组. 
   Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
   new Rect(), new Flower()}; 
   for (Shape shape : shapes) { 
   shape.draw(); 
 } 
} 

什么叫 "圈复杂度" ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很 多的条件分支或者循环语句, 就认为理解起来更复杂.

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一 个方法的圈复杂度太高, 就需要考虑重构

3) 可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape { 
   @Override 
   public void draw() { 
   System.out.println("△"); 
 } 
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高

向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途

// Animal.java 
public class Animal { 
   protected String name; 
   public Animal(String name) { 
   this.name = name; 
 } 
   public void eat(String food) { 
   System.out.println("我是一只小动物"); 
   System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
public class Bird extends Animal { 
   public Bird(String name) { 
   super(name); 
 } 
   public void eat(String food) { 
   System.out.println("我是一只小鸟"); 
   System.out.println(this.name + "正在吃" + food); 
 }
public void fly() { 
   System.out.println(this.name + "正在飞");
   }
}

super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用 super 关键字.

super 表示获取到父类实例的引用. 涉及到两种常见用法.

1) 使用了 super 来调用父类的构造器(这个代码前面已经写过了)

public Bird(String name) { 
 super(name); 
} 

2) 使用 super 来调用父类的普通方法

public class Bird extends Animal { 
   public Bird(String name) { 
   super(name); 
 } 
   @Override 
   public void eat(String food) { 
   // 修改代码, 让子调用父类的接口. 
   super.eat(food); 
   System.out.println("我是一只小鸟"); 
   System.out.println(this.name + "正在吃" + food); 
 } 
} 

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法

注意 super 和 this 功能有些相似, 但是还是要注意其中的区别

七万字千锤百炼,Java基础知识大总结_第111张图片

在构造方法中调用重写的方法

class B { 
 public B() { 
    // do nothing 
    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 对象的同时, 会调用 B 的构造方法.

B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func

此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0

结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发 动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题

总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带 来的编码上的好处

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系

C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了.

Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系.

Go 语言中没有 "继承" 这样的概念, 同样也能表示多态.

抽象类

语法规则

在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class)

abstract class Shape { 
 abstract public void draw(); 
}

在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体 代码).

对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类

注意事项

1) 抽象类不能直接实例化

Shape shape = new Shape(); 
// 编译出错
//Error:(30, 23) java: Shape是抽象的; 无法实例化

2) 抽象方法不能是 private 的

abstract class Shape { 
 abstract private void draw(); 
} 
// 编译出错
//Error:(4, 27) java: 非法的修饰符组合: abstract和private 

3) 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写, 也可以被子类直接调用

abstract class Shape { 
   abstract public void draw(); 
   void func() { 
   System.out.println("func"); 
 } 
} 
  class Rect extends Shape { 
   ... 
} 
public class Test { 
  public static void main(String[] args) { 
  Shape shape = new Rect(); 
  shape.func(); 
 } 
} 
// 执行结果
//func

抽象类的作用

抽象类存在的最大意义就是为了被继承.

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

接口

接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含 静态常量.

语法规则

在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口

interface IShape { 
   void draw(); 
} 
class Cycle implements IShape { 
   @Override 
   public void draw() { 
   System.out.println("○"); 
 } 
} 
public class Test { 
   public static void main(String[] args) { 
   IShape shape = new Rect(); 
   shape.draw(); 
 } 
} 

使用 interface 定义一个接口

接口中的方法一定是抽象方法, 因此可以省略 abstract

接口中的方法一定是 public, 因此可以省略 public

Cycle 使用 implements 继承接口. 此时表达的含义不再是 "扩展", 而是 "实现"

在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.

接口不能单独被实例化

扩展(extends) vs 实现(implements)

扩展指的是当前已经有一定的功能了, 进一步扩充功能.

实现指的是当前啥都没有, 需要从头构造出来

接口中只能包含抽象方法. 对于字段来说 , 接口中只能包含静态常量(final static).

interface IShape { 
   void draw(); 
   public static final int num = 10; 
} 

其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量.

1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.

2. 接口的命名一般使用 "形容词" 词性的单词.

3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性

实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的. 然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果. 现在我们通过类来表示一组动物

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 Cat extends Animal implements IRunning { 
   public Cat(String name) { 
   super(name); 
 } 
   @Override 
   public void run() { 
   System.out.println(this.name + "正在用四条腿跑"); 
 } 
} 
class Fish extends Animal implements ISwimming { 
   public Fish(String name) { 
   super(name); 
 } 
   @Override 
   public void swim() { 
   System.out.println(this.name + "正在用尾巴游泳"); 
 } 
} 
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 + "正在蹬腿游泳"); 
 } 
} 

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口

这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而 只关注某个类是否具备某种能力.

接口使用实例

给对象数组排序

给定一个学生类

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 "[" + this.name + ":" + this.score + "]"; 
 } 
} 

再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序)

Student[] students = new Student[] { 
 new Student("张三", 95), 
 new Student("李四", 96), 
 new Student("王五", 97), 
 new Student("赵六", 92), 
};

让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法

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 "[" + this.name + ":" + this.score + "]"; 
 } 
   @Override 
   public int compareTo(Object o){ 
   Student s = (Student)o; 
   if(this.score > s.score){ 
   return -1;
 }else if (this.score < s.score){ 
   return 1; 
 }else{ 
   return 0; 
 } 
 } 
} 

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象. 然后比较当前对象和参数对象的大小关系(按分数来算)

如果当前对象应排在参数对象之前, 返回小于 0 的数字;

如果当前对象应排在参数对象之后, 返回大于 0 的数字;

如果当前对象和参数对象不分先后, 返回 0

// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力. 通过 重写 compareTo 方法的方式, 就可以定义比较规则.

为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)

public static void sort(Comparable[] array) { 
 for(int bound = 0; bound < array.length; bound++){ 
    for (int cur = array.length - 1; cur > bound; cur--){ 
 if(array[cur - 1].compareTo(array[cur]) > 0) { 
   // 说明顺序不符合要求, 交换两个变量的位置
    Comparable tmp = array[cur - 1]; 
    array[cur - 1] = array[cur]; 
    array[cur] = tmp; 
   } 
  } 
 } 
} 

再次执行代码

sort(students); 
System.out.println(Arrays.toString(students)); 
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]] 

总结

核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不 能包含普通方法, 子类必须重写所有的抽象方法.

你可能感兴趣的