课程介绍

Java学习体系

pass

Java的三个版本

  • Java SE(Java Standard Edition)标准版
  • Java EE(Java Enterprise Edition)企业版
  • Java ME(Java Micro Edition)小型版

Java8 和 Java11 是 Java 的两个长期维护版,一般选择这两个版本来进行学习和使用.

Java & JDK & JRE

  • JRE 基本介绍

    1. JRE 全称 Java Runtime Environment,即 Java 运行环境

      JRE = JVM + Java 的核心类库

    2. JRE 包括 Java 虚拟机 (JVM, Java Virtual Machine) 和 Java 程序所需要的核心类库等. 如果只想运行一个开发好的 Java 程序,只需要安装 JRE 即可.

  • JDK 基本介绍

    1. JDK 全称 Java Development Kit,即 Java 开发工具包

      JDK = JRE + Java 开发工具(Java,Javac,Javadoc,Javap等)

    2. JDK 是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具,也包括了 JRE。所以安装了 JDK 就不用再单独安装 JRE 了.

Java JDK 安装

Java Downloads | Oracle

  • 选择 Java SE -> Java 8 -> Windows -> x64 Installer (jdk-8uxxx-windows-x64.exe) 下载并进行安装 (需要登陆Oracle账号)

  • 在环境变量中添加安装根目录的路径,命名为 JAVA_HOME,并在 Path 中添加%JAVA_HOME%\bin

  • cmd 中输入 javac -versionjava -version 检查是否正常输出,正常输出则环境变量配置正确.

Note:环境变量中有上下两个区域,上面的 User variables for xxx 和下面的 System variables. 如果是在上面配置的环境变量则只对当前用户生效,如果是在下面配置的环境变量则是对所有用户生效。

Hello World

  • 第一个程序Hello.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1.public class Hello 表示Hello是一个类,是一个public公有的类
//2.Hello{}表示一个类的开始和结束
//3.public static void main(String[] args)表示一个主方法,即我们程序的入口
//4.main() {} 表示方法的开始和结束
//5.System.out.println("Hello,World!");表示输出“Hello,World!”到屏幕
//6.分号;表示语句的结束

public class Hello
{
//编写一个main方法
public static void main(String[] args)
{
System.out.println("Hello,World!");
}
}
  • 编译运行代码

将上面代码保存为Hello.java文件,在文件保存路径打开cmd (地址栏输入cmd回车)

执行javac Hello.java对文件进行编译,生成一个Hello.class文件

执行java Hello运行生成的Hello.class文件,得到输出Hello,World!

注意

  1. 运行Hello.class时,应该使用命令java Hello而不是java Hello.class,否则会报错

    因为程序会以为你要运行一个叫做Hello.class的类,但其实这个类叫做Hello

    1
    2
    >java Hello.class
    Error: Could not find or load main class Hello.class
  2. 当源文件中定义了public类,则文件名必须和类名一致。此源文件中我们定义了public类Hello,则文件必须命名为Hello.java

    如果将文件命名为helloworld.java则会如下报错

    1
    2
    3
    4
    5
    >javac helloworld.java
    helloworld.java:8: error: class Hello is public, should be declared in a file named Hello.java
    public class Hello
    ^
    1 error
  3. 其中要设置中文编码为GBK格式,因为在cmd中,中文编码采用GBK格式。如果文件采用UTF-8编码,则运行会报错

采用utf-8,在cmd中编译报错信息

1
2
3
4
5
6
7
8
9
Hello.java:1: error: unmappable character for encoding GBK
//1.public class Hello 琛ㄧずHello鏄竴涓被锛屾槸涓?涓猵ublic鍏湁鐨勭被
^
......

Hello.java:10: error: unmappable character for encoding GBK
//缂栧啓涓?涓猰ain鏂规硶
^
6 errors

Java运行机制

.java文件称为源文件,经过javac编译得到的.class文件称为字节码文件,字节码文件是可以被Java虚拟机(JVM)识别的文件,使用java命令可以将.class字节码文件装载到Java虚拟机中执行,最后得到结果。在源文件被修改之后,需要重新编译执行才能得到新的结果。

Java开发注意事项和细节说明

  1. Java源文件以.java为扩展名。

  2. 源文件的基本组成部分是类(class),如本类中的Hello类。

  3. Java应用程序的执行入口是main()方法。它有固定的书写格式:

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

  4. Java语言严格区分大小写。

  5. Java方法由一条一条的语句构成,每个语句以”;“结束。

  6. 大括号都是成对出现的,缺一不可。应养成先配对括号再在括号中写代码的习惯。

  7. 一个源文件中最多只能有一个public类,其它类的个数不限。

  8. 如果源文件包含一个public类,则文件名必须按该类命名!

  9. 一个源文件中最多只能有一个public类,其它类的个数不限。也可以在非public类中定义main函数,然后指定运行非puclic类,这样程序的入口就是非public的main方法。

转义字符

转义字符 功能 转义字符 功能
\t 水平制表符 \n 换行符
\\ 输出一个\ \" 输出一个"
\' 输出一个' \r 输出一个回车
  • 关于\t的说明,\t表示跳到下一个8位,中间以空格填补
1
2
3
System.out.println("123456781234567812345678");
System.out.println("11\t222\t333");
System.out.println("111111111\t22222\t");
1
2
3
4
5
123456781234567812345678
11 222 333
^ ^ ^
111111111 22222
^ ^
  • 关于\r的说明,\r表示回车,但是不换行,光标回到当前行的最前面
1
2
3
4
5
6
7
8
9
10
System.out.println("123456\r78");
// 输出123456
// 回车,光标回到当前行的最前面
// 78覆盖12
// 最后输出结果
783456
// 使用`\r\n`
System.out.println("123456\r\n78");
123456
78

初学者易犯错误

语法错误

  1. 找不到文件

    可能原因:源文件名书写错误或者当前路径错误

  2. 主类名和文件名不一致

  3. 忘记添加分号,缺少大括号,引号等

  4. 区分1l0O

  5. 非法字符:区分中文符号和英文符号

  6. 拼写错误

高级错误

  1. 逻辑错误
  2. 环境错误

注释

  1. 单行注释——\\

  2. 多行注释 ——/* ... */

  3. 文档注释——Java Doc,文档注释在开发中较为常见

    1
    2
    3
    4
    /**
    *@
    *@
    */

Java代码规范

  1. 类、方法的注释要以javadoc的方法来写

  2. 非Java Doc的注释,往往是给代码的维护者看的,着重告诉维护者为什么要这么写,如何修改,注意问题等

  3. 使用tab操作,实现缩进。tab实现选中内容整体右移,shift+tab实现 选中内容整体左移

  4. 运算符和 = 两边习惯性各家一个空格。比如2 + 4 * 5 = 22

  5. 源文件使用utf-8编码

  6. 行宽度不要超过80字符

  7. 代码编写次行风格和行尾风格

    • 次行风格(阅读方便)
    1
    2
    3
    4
    public static void main(String[] args)
    {
    System.out.println("Hello World!");
    }
    • 行尾风格(书写方便,推荐风格)
    1
    2
    3
    public static void main(String[] args){
    System.out.println("Hello World!");
    }

Java API文档

  • API, Application Programming Interface, 应用程序编程接口时Java提供的基本编程接口(Java提供的类还有相关的方法)
  • Java语言提供了大量的基础类,因此Oracle公司也为这些基础类提供了相应的API文档,用于告诉开发者如何使用这些类,以及这些类里包含的方法
  • Java8 API在线文档
  • Java API的组织结构
graph TD;
    JDK8,11-->包1;
    JDK8,11-->包2;
    JDK8,11-->包...;
    包2-->接口;
    包2-->类1;
    包2-->类...;
    包2-->异常;
    类1-->字段;
    类1-->构造方法;
    类1-->成员方法;

变量

  • 变量表示内存中的一个存储区域,不同变量,类型不同,占用的存储空间大小不同
  • 变量要有自己的名称【变量名】和类型【数据类型】
  • 变量的值可以在变量数据类型对应的范围上变化
  • 变量必须先声明才可以使用
  • 变量在同一作用域内不能重名
  • 变量 = 变量名 + 值 + 数据类型,这三点被称为变量三要素

Java数据类型

  • 基本数据类型
    • 数值型
      • 整数型:byte[1],short[2],int[4],long[8]
      • 浮点型:float[4],double[8]
    • 字符型:char[2],存放单个字符
    • 布尔型:boolean[1],存放truefalse
  • 引用数据类型
    • 类(class):String
    • 接口(interface)
    • 数组([])
graph LR
Java数据类型-->基本数据类型;
Java数据类型-->引用数据类型;
基本数据类型-->数值型;
基本数据类型-->字符型;
基本数据类型-->布尔型;
引用数据类型-->类;
引用数据类型-->接口interface;
引用数据类型-->数组;
数值型-->整数类型:byte,short,int,long;
数值型-->浮点类型:float,double;
字符型-->char;
布尔型-->boolean;
类-->String;

整数类型

  • byte(字节)是计算机的基本存储单位,bit是计算机的最小存储单位,1 byte = 8 bits

    1
    byte a = 3;  //1个byte,8bits
  • Java的整数常量默认数据类型为int型,long型常量必须在数字后加l或者L

    1
    long a = 1L;
  • Java程序中整型常量声明为int型,除非不足以表示大数,才使用long

浮点类型

  • 浮点数在机器中的存储形式:符号位+指数位+尾数位

  • 尾数位部分可能丢失,造成精度损失

  • Java的浮点型常量默认位double型,声明float型常量,必须在数字后加f或者F,声明double类型的常量在数字后面可以加dD,也可以不加

  • 浮点数常量的两种形式

    • 十进制数形式:3.143.14f.314
    • 科学计数法:3.14e2314.0)、3.14e-20.0314
  • 通常情况下应该使用double类型来存储浮点数,因为它的精度更高

  • 浮点数使用陷阱——不要对运算结果是小数的式子进行相等判断

    1
    2
    System.out.println(8.1 / 3)
    //2.6999999999999997

    8.1 / 3得到的结果并不是2.7,而是一个接近2.7的数,这种情况常发生在除数为3的倍数的式子上

字符类型

  • 字符常量是用单引号括起来的单个字符,例如'A''张''9'

  • Java中还允许使用抓你字符\来将其后的字符转变为特殊字符型常量。例如\n\t

  • 在Java中,char的本质是一个整数,在输出时,是unicode码对应的字符。

    Unicode编码转换工具

  • 可以直接给char赋一个整数,将输出整数对应的unicode字符

  • char型可以强制类型转换为整型

  • char型可以进行运算,相当于一个整数

  • 字符和码值的对应关系是按照字符编码表对应好的

  • 常用字符编码

    • ASCII编码,每个字符用1个字节表示,一共128个字符(1个字节最多可以表示256个字符,但是ASCII的设计者只使用了128个)
    • Unicode编码,每个字符用2个字节表示,可以表示汉字,字母和汉字统一都使用2个字节,最大表示65536个字符,兼容ASCII码
    • UTF-8编码表,大小可变的编码,它使用1-6个字节来表示一个字符,根据不同的字符变化字节的长度。字母使用1个字节,汉字使用3个字节
    • GBK编码,可以表示汉字,而且范围广,字母使用1个字节,汉字使用2个字节,GBK码表示的汉字范围没有UTF-8广
    • GB2312码,可以表示汉字,gb2312<gbk
    • BIG5码,可以表示繁体中文

布尔类型

  • 布尔类型也叫boolean类型,boolean类型数据只运行取值为truefalse,不能为空
  • boolean类型占一个字节,注意是一个字节,8个比特
  • 注意,和C语言不同,在Java中不可以使用0或者非0的数来代替falsetrue

自动类型转换

  • 当Java程序在进行赋值或者运算时,精度小的类型回自动转换为进度大的数据类型,这个机制就是自动类型转换

  • 数据类型按进度(容量)排序,左边可以自动类型转换为右边

      graph LR
    char --> int --> long --> float --> double
    byte --> short --> int
  • 有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的数据类型,然后再进行计算

  • 当把精度大的数据类型赋值给精度小的数据类型时,就会报错,反之会进行自动数据类型转换

  • 当把一个整数赋值给byteshort时,编译器会先判断整数的值是否在byteshort的范围内,如果在则可以赋值,如果不在则会再判断数据类型是否可以自动

  • 转换

    1
    2
    3
    4
    byte b = 10; //可以赋值
    byte b = 257;//报错
    //报错信息
    error: incompatible types: possible lossy conversion from int to byte
  • 当用一个变量byteshort进行赋值时,编译器会直接判断变量的类型是否匹配以及是否可以自动类型转换,当不匹配的时候会报错

    1
    2
    3
    4
    int n = 10;
    byte b = n; //报错
    //错误信息
    error: incompatible types: possible lossy conversion from int to byte
  • byte–>short)和char之间不会相互自动转换

    1
    2
    3
    4
    byte b = 10;
    char c = b; //报错
    //错误信息
    error: incompatible types: possible lossy conversion from byte to char
  • byteshortchar他们三者间可以进行运算,在运算时首先转换为int类型。即使是bytebyteshortshortcharchar之间的运算,结果也是int类型

    1
    2
    3
    4
    byte b = 1;  //b为byte型,b+b为int型
    char c = 2; //c为char型,c+c为int型
    short s = 3; //s为short型,s+s为int型
    // b + c + s 结果为int型
  • boolean不参与类型的转换,其不能自动转换为任意数据类型,也不能强制转换为任意数据类型

    1
    2
    3
    4
    boolean b = true;
    int n = (int)b; //报错
    //报错信息
    error: incompatible types: boolean cannot be converted to int
  • 自动提升的原则:表达式结果的类型自动提升为操作数中容精度最高的类型

强制类型转换

  • 强制数据类型转将精度高的数据类型转换为精度低的数据类型,需要在数据前面加上强制类型转换符(int)(float)(char)

  • 在强制类型转换的过程中会造成精度损失数据溢出

    1
    2
    3
    4
    5
    6
    7
    //精度损失
    int n = (int)3.14;
    System.out.println(n); // 3

    //数据溢出
    byte b = (byte)2000;
    System.out.println(b); // -48
  • 强制转换符号只针对最近的操作数有效,往往会使用小括号提升运算式的优先级

    1
    2
    int x = (int)10*3.5 + 6*1.5;   //报错
    int y = (int)(10*3.5 + 6*1.5); //44
  • char型可以保存int的常量值,但不能保存int型的变量值,需要强制类型转换

    1
    2
    3
    4
    5
    6
    7
    // 直接赋数值时会先判断范围
    char c1 = 100; //正确
    // 不能自动转换
    int n = 100;
    char c2 = n; //报错
    // 可以强制转换
    char c3 = (char)n //正确,输出'd'

基本数据类型和字符串间的转换

  • 在基本数据类型转字符串——+ ""即可

    1
    2
    3
    100 + "";
    4.5 + "";
    true + "";
  • 字符串转基本数据类型——包装类

    1
    2
    3
    4
    5
    6
    7
    8
    int n = Integer.parseInt("123");         //String-->int
    byte e = Byte.parseByte("123"); //String-->byte
    short s = Short.parseShort("123"); //String-->short
    long l = Long.parseLong("123"); //String-->long
    float f = Float.parseFloat("3.14"); //String-->float
    double d = Double.parseDouble("3.14"); //String-->double
    boolean b = Boolean.parseBoolean("true");//String-->boolean

  • 在将String类型转成基本数据类型时,要确保String类型能够转成有效的数据,比如我们可以把字符串"123"转成一个整数,但是不能把"hello"转换成一个整数

  • 如果将"hello"转换成int,编译时不会报错,运行会出现异常(Exception)

    1
    2
    3
    4
    5
    6
    7
    int n = Integer.parseInt("hello");
    //编译通过,运行异常
    Exception in thread "main" java.lang.NumberFormatException: For input string: "hello"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:580)
    at java.lang.Integer.parseInt(Integer.java:615)
    at Hello.main(Hello.java:13)

运算符

算数运算符

运算符 作用 运算符 作用
+ 正号 - 负号
+ -
* /
% 取模 + 字符串拼接
++ 前自增/后自增 -- 前自减/后自减

整数除法

  • 整数和整数类型的除法运算结果是一个整数

    1
    2
    3
    System.out.println(10 / 4);       // 2   int
    System.out.println(10.0 / 4); // 2.5 double
    System.out.println(10 / 4 * 1.0); // 2.0 double

取模运算

  • Java取模运算的本质是

    1
    a % b = a - a / b * b 
    1
    2
    3
    4
    5
    6
    7
    8
    // 10 % 3 = 10 - 10 / 3 * 3 = 10 - 3 * 3 = 1
    System.out.println(10 % 3); // 1
    //-10 % 3 = -10 - (-10)/3*3 = -10 - (-3) * 3 = -1
    System.out.println(-10 % 3); // -1
    //10 % -3 = 10 - 10/(-3)*(-3) = 10 - (-3)*(-3) = 1
    System.out.println(10 % -3); // -1
    //-10 % -3 = -10 - (-10)/(-3)*(-3) = -10 - 3*(-3) = -1
    System.out.println(-10 % -3);// -1

    余数的符号要和被除数的符号一致

  • 当参与取模运算的数为浮点数时,取模运算的计算公式如下

    1
    a % b = a - (int)a / b * b 
    1
    2
    3
    4
    5
    // -10.5 % 3 = -10.5 - (int)(-10.5) / 3 * 3
    // = -10.5 - (-10) / 3 * 3
    // = -10.5 + 9
    // = -1.5
    System.out.println(-10.5 % 3); // -1.5

自增运算

  • 前++和后++

    作为表达式使用,前++是先自增再赋值,后++是先赋值再自增

    1
    2
    int i = 1;
    int j = i++; // j=1,i=2
    1
    2
    int i = 1; 
    int j = ++i; // j=2,i=2
  • 经典面试题1

    1
    2
    3
    int i = 1;
    i = i++;
    System.out.println(i);
    1
    2
    3
    // 结果是1
    在执行 i = i++; 这个语句时,Java使用了临时变量
    (1).temp = i; (2).i = i + 1; (3).i = temp;
  • 经典面试题2

    1
    2
    3
    int i = 1;
    i = ++i;
    System.out.println(i);
    1
    2
    3
    // 结果是2
    在执行 i = ++i; 这个语句时,Java使用了临时变量
    (1).i = i + 1; (2).temp = i; (3).i = temp;
  • 课后题

    1
    2
    int i  = 1;
    System.out.println(++i+i); //4
    1
    2
    int i = 1;
    System.out.println(i+++i); //3
    1
    2
    int i  = 1;
    System.out.println(i+i++); //2

加号的使用

  • 当加号两边都是数值型时,做加法运算

    1
    System.out.println(100+3+98);//201
  • 当加号两边有一边是字符串型时,做拼接运算

    1
    2
    System.out.println(100+3+"98");//10398
    System.out.println("100"+3+98);//100398
  • 字符型相加

    1
    2
    System.out.println("男"+"女"); //男女   String型
    System.out.println('男'+'女'); //52906 int型

关系运算符

== != < > <= >= instanceof
相等 不相等 小于 大于 小于等于 大于等于 检查是否是类的对象

逻辑运算符

& | ^ && || !
逻辑与 逻辑或 逻辑异或 短路与 短路或 逻辑非
  • 逻辑与&和短路与&&的区别

    • 对于短路与&&而言,如果第一个条件为false,则直接输出false,后面的条件不会再判断
    1
    2
    3
    4
    int a = 1;
    int b = 1;
    System.out.println(a < 0 && ++b == 2); // false
    System.out.println(b); // 1, ++b没有执行
    • 对于逻辑与&而言,如果第一个条件为false,后面的条件还会再判断
    1
    2
    3
    4
    int a = 1;
    int b = 1;
    System.out.println(a < 0 & ++b == 2); // false
    System.out.println(b); // 2, ++b执行了
  • 逻辑或|和短路或||的区别

    • 对于短路或||而言,如果第左边的条件为true,则直接输出true,后面的条件不会再判断
    1
    2
    3
    4
    int a = 1;
    int b = 1;
    System.out.println(a > 0 && ++b == 2); // true
    System.out.println(b); // 1, ++b没有执行
    • 对于逻辑或|而言,如果第左边的条件为true,后面的条件还会再判断
    1
    2
    3
    4
    int a = 1;
    int b = 1;
    System.out.println(a > 0 & ++b == 2); // true
    System.out.println(b); // 2, ++b执行了
  • 逻辑非!和逻辑异或^

    • 逻辑非将true输出false,将false输出true
    • 逻辑异或两边相同输出false,两边不同输出true

赋值运算符

= += -= *= /= %=
  • 复合数据类型的赋值运算会进行数据类型的强制转换

    1
    2
    3
    byte b = 0;
    b += 1; // 正确执行,等价于 b = (byte)(b + 1)
    b++; // 正确执行,等价于 b = (byte)(b + 1)
    1
    2
    b = b + 1;  // error
    // error: incompatible types: possible lossy conversion from int to byte

三元运算符

?: 三元运算符
1
[条件表达式] ? [表达式1] : [表达式2]
  • 如果条件表达式为true,则输出表达式1的结果;

    如果条件表达式为false,则输出表达式2的结果;

    1
    2
    int a = 0;
    System.out.println(a == 0 ? ++a : --a); // 1, 输出表达式1
    1
    2
    int a = 0;
    System.out.println(a != 0 ? a++ : a--); //-1, 输出表达式2
  • 三元运算符用在赋值运算时,表达1和表达式2的结果要和被赋值变量的类型匹配,否则需要强制转换

    1
    2
    3
    4
    5
    int a = true ? 1.0 : 1.0;      // error
    int a = true ? (int)1.0 : 1.0; // error
    int a = true ? 1.0 : (int)1.0; // error
    //error: incompatible types: possible lossy conversion from double to int
    int a = true ? (int)1.0 : (int)1.0; //执行成功
  • 三元运算符也可以使用if...else...语句改写

  • 返回三个数中的最大值

    1
    2
    max = a > b ? (a > c ? a : c) : (b > c ? b : c);
    max = (a > b ? a : b) > c ? (a > b ? a : b) : c;

字符串的比较

使用equals方法

equals方法会逐个比较两个字符串每个字符是否相同。如果两个字符串内容完全相同,它返回 true,否则返回 false

1
2
3
System.out.println("林黛玉".equals("贾宝玉")); //false

System.out.println("林黛玉".equals("林黛玉")); //true

使用 == 做比较

  • 基本数据类型:比较的是数据值是否相同
1
2
3
String Lin = "林黛玉";             //创建字符串变量
System.out.println(Lin == "林黛玉"); //true
System.out.println(Lin.equals("林黛玉")); //true
  • 引用数据类型:比较的是地址值是否相同
1
2
3
String Lin = new String("林黛玉"); //创建字符串对象
System.out.println(Lin == "林黛玉"); //false
System.out.println(Lin.equals("林黛玉")); //true

此时Lin中存储的是一个地址,地址所指向的String内存空间中存储着"林黛玉",而"林黛玉"是基本数据类型

所以Lin == "林黛玉"是一串地址和"林黛玉"的比较,返回false

  • 字符串数组也是引用类型,但是使用[]访问地址之后,name[0]对应的是内容
1
2
String[] names = {"林黛玉"};
System.out.println(names[0] == "林黛玉"); //true

运算符的优先级

. () {} ; ,
R—>L ++ -- ~ !
L—>L * / %
L—>L + -
L—>L << >> >>>
L—>L < > <= >= instanceof
L—>L == !=
L—>L &
L—>L ^
L—>L |
L—>L &&
L—>L ||
L—>L ? :
R—>L = *= /= %=
R—>L += -= <<= >>=
R—>L >>>= &= ^= |=

标识符命名规则和规范

  • 标识符的概念

    Java对各种变量、方法和类的命名时使用的字符序列称为标识符

    凡是可以自己起名字的地方都叫标识符

  • 标识符的命名规则

    1. 由26个英文字母大小写,0-9,下划线_和$组成
    2. 数字不可以开头,字母下划线和$可以开头
    3. 不可以使用关键字和保留字
    4. Java中严格区分大小写,长度无限制
    5. 标识符不能包含空格
  • 标识符命名规范

    1. 包名:多个单词组成时所有字母都小写,如java.lang、ava.sql
    2. 类名、接口名:多单词组成时,所有单词的首字母大写,如TankShotGame(大驼峰)
    3. 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单子首字母大写,如tankShotGame,stuName(小驼峰)
    4. 常量名:所有字母都大写,多单词时每个单词用下划线连接,如XXX_YYY_ZZZ
  • 关键字:被Java语言赋予特殊含义,用做专门用途的字符串单词,关键字中的所有字母都是小写

  • 保留字:现有Java版本尚未使用,但以后版本可能会作为关键字使用的字符串单词

键盘输入

  • 使用Scanner类来获取键盘输入

    Scanner类是一个简单的文本扫描器

    1
    import java.util.Scanner; // 导入java.util包下的Scanner类

    创建一个Scanner对象

    1
    Scanner myScanner = new Scanner(System.in); //System.in是标准输入流

    使用Scanner类的next方法读取字符串

    1
    String name = myScanner.next(); // 从System.in读取一个字符串并赋给String型变量name

    使用Scanner类的nextInt方法读取整数

    1
    int age = myScanner.nextInt(); //从System.in读取一个int型整数并赋给变量age

    使用Scanner类的nextDouble方法读取浮点数

    1
    double sal = myScanner.nextDouble(); //从System.in读取一个double型浮点数并赋给变量sal

进制

  • 计算机中,对于整数有四种表示方法
    1. 二进制,0b开头
    2. 十进制,非零数字开头
    3. 八进制,0开头
    4. 十六进制,0x开头

进制的转换(基本功)

  • 第一组

    1. 二进制转十进制
    2. 八进制转十进制
    3. 十六进制转十进制
  • 第二组

    1. 十进制转二进制
    2. 十进制转八进制
    3. 十进制转十六进制
  • 第三组

    1. 二进制转八进制
    2. 二进制转十六进制
  • 第四组

    1. 八进制转二进制
    2. 十六进制转二进制

其它进制转十进制

  • 二进制转十进制

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和

例如将0b1011转成十进制

1×23+0×22+1×21+1×20=11\boldsymbol{1} \times 2^3 + \boldsymbol{0} \times 2^2 + \boldsymbol{1} \times 2^1 + \boldsymbol{1} \times 2^0 = 11

  • 八进制转十进制

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和

例如将0234转成十进制

2×82+3×81+4×80=156\boldsymbol{2} \times 8^2 + \boldsymbol{3} \times 8^1 + \boldsymbol{4} \times 8^0 = 156

  • 十六进制转十进制

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和

例如将0x23A转成十进制

2×162+3×161+10×160=570\boldsymbol{2} \times 16^2 + \boldsymbol{3} \times 16^1 + \boldsymbol{10} \times 16^0 = 570

十进制转其它进制

  • 十进制转二进制

规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来就是对应的二进制

例如将34转成二进制

34÷2=17017÷2=818÷2=404÷2=202÷2=101÷2=01\begin{split} 34 \div 2 &= 17\cdots 0 \\ 17 \div 2 &= 8 \cdots 1 \\ 8 \div 2 &= 4 \cdots 0 \\ 4 \div 2 &= 2 \cdots 0 \\ 2 \div 2 &= 1 \cdots 0 \\ 1 \div 2 &= 0 \cdots 1 \end{split}

结果是100010,由于一个字节是8位二进制,所以前面还要加两个0来填充高位得到0010 0010,再加上二进制的标识符,最后结果为0b 0010 0010

  • 十进制转八进制

规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来就是对应的八进制

例如将131转成八进制

131÷8=16316÷8=202÷8=02\begin{split} 131 \div 8 &= 16\cdots 3 \\ 16 \div 8 &= 2 \cdots 0 \\ 2 \div 8 &= 0 \cdots 2 \end{split}

结果是203,加上八进制的标识符最后结果为0203

  • 十进制转十六进制

规则:将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来就是对应的十六进制

例如将237转成十六进制

237÷16=1413(D)14÷16=014(E)\begin{split} 237 \div 16 &= 14\cdots 13(D) \\ 14 \div 16 &= 0 \cdots 14(E) \\ \end{split}

结果是ED,加上十六进制的标识符最后结果为0xED

二进制转八进制和十六进制

  • 二进制转八进制

规则:从低位开始,将二进制数每三位一组,转成对应的八进制数即可

例如将0b11010101转成八进制

0b(11)(010)(101)=0(3)(2)(5)=03250b(11)(010)(101) = 0(3)(2)(5) = 0325

  • 二进制转十六进制

规则:从低位开始,将二进制数每四位一组,转成对应的十六进制数即可

例如将ob11010101转成十六进制

0b(1101)(0101)=0x(13)(5)=0xD50b(1101)(0101) = 0x(13)(5) = 0xD5

八进制和十六进制转二进制

  • 八进制转二进制

规则:将八进制数的每一位,转成对应的一个三位的二进制数

例如将0237转成二进制

0237=0b(010)(011)(111)=ob100111110237 = 0b(010)(011)(111) = ob 10 011 111

注意二进制八位补齐

  • 十六进制转二进制

规则:将十六进制数的每一位,转成对应的一个四位的二进制数

例如将0x23B转成二进制

0x23B=0b(0010)(0011)(1011)=ob0010001110110x23B = 0b(0010)(0011)(1011) = ob 0010 0011 1011

位运算

原码、反码、补码(重难点)

  1. 二进制的最高位是符号位:0表示正数,1表示负数
  2. 正数的原码、反码、补码都一样(三码合一)
  3. 负数的反码 = 它的原码符号位不变,其它位取反
  4. 负数的原码 = 它的反码符号位不变,其它位取反
  5. 负数的补码 = 它的反码 + 1
  6. 负数的反码 = 它的补码 - 1
  7. 0的反码和补码都是0
  8. Java没有无符号数,换言之,Java中的数都是由符号的
  9. 在计算机在底层运算的时候,都是以补码的方式来运算的
  10. 当我们看到运算结果的时候,看到的是它的原码

位运算符

  • Java中有7个位运算符
& | ^ ~ >> << >>>
按位与 按位或 按位异或 按位取反 算数右移 算数左移 逻辑右移
无符号右移
两位全为1,结果为1,否则为0 两位全为0,结果为0,否则为1 两位一个为0一个为1,结果为1,否则为0 0变1,1变0 低位溢出,符号位不变,并用符号位补溢出的高位 符号位不变,低位补 0 低位溢出,高位补0

原码反码补码实例

  • 例子——计算2&3的结果

    1. 先写出2和3的原码

      1
      2
      2的原码 = 00000000 00000000 00000000 00000010;
      3的原码 = 00000000 00000000 00000000 00000011;
    2. 根据原码写出补码

      1
      2
      2的补码 = 00000000 00000000 00000000 00000010;
      3的补码 = 00000000 00000000 00000000 00000011;
    3. 补码参与按位与运算&

      1
      2
      3
      2的补码   = 00000000 00000000 00000000 00000010;
      3的补码 = 00000000 00000000 00000000 00000011;
      2&3的补码 = 00000000 00000000 00000000 00000010;
    4. 根据运算后的补码写出原码

      1
      2&3的原码 = 00000000 00000000 00000000 00000010;
    5. 根据运算后的原码(二进制)转换成十进制

      1
      2&3 = 00000000 00000000 00000000 00000010 = 2
  • 例子——计算~-2的结果

    1. 先写出-2的原码

      1
      -2的原码 = 10000000 00000000 00000000 00000010;
    2. 根据-2的原码写出-2的反码(负数反码 = 符号位不变其它位取反)

      1
      -2的反码 = 11111111 11111111 11111111 11111101
    3. 根据反码写出补码(负数的补码 = 负数的反码 + 1)

      1
      -2的补码 = 11111111 11111111 11111111 11111110
    4. 补码参与按位取反运算~

      1
      ~-2的补码 = 00000000 00000000 00000000 00000001
    5. 将运算后的补码写成原码(正数三码合一)

      1
      ~-2的原码 = 00000000 00000000 00000000 00000001
    6. 将运算后的原码转换成十进制

      1
      ~-2 = 00000000 00000000 00000000 00000001 = 1
  • 例子——计算~2的结果

    1. 由正数的三码合一,直接得到2的补码

      1
      2的补码 = 00000000 00000000 00000000 00000010;
    2. 补码参与按位取反运算~

      1
      ~2的补码 = 11111111 11111111 11111111 11111101;
    3. 运算后的补码是一个负数,计算它的原码

      1
      2
      ~2的反码 = 11111111 11111111 11111111 11111100;
      ~2的原码 = 10000000 00000000 00000000 00000011;
    4. 转换成十进制

      1
      ~2 = -3

位运算符

  • 算数右移>>:低位溢出,符号位不变,并用符号位补溢出的高位

    算数左移<<:符号位不变,低位补 0

    1
    2
    3
    4
    5
    计算1>>2
    1 => 00000000 00000000 00000000 00000001;
    => 00000000 00000000 00000000 00000000 01;//低位溢出,符号位不变
    => 00000000 00000000 00000000 00000000; //符号位补溢出高位
    //相当于 1 / 2 /2 = 0
    1
    2
    3
    4
    5
    计算1<<2
    1 => 00000000 00000000 00000000 00000001;
    => 00000000 00000000 00000000 00000100;//低位补0,符号位不变
    => 00000000 00000000 00000000 00000000;//符号位补溢出高位
    //相当于 1 * 2 * 2 = 4
    1
    2
    计算15>>2
    15 / 2 / 2 = 3

控制结构

顺序控制

分支控制

if…else…

1
2
3
4
if(条件表达式)
{
代码块;
}
  • 当条件表达式为true时执行代码块,为false时不执行代码块
  • 条件表达式的输出结果必须为boolean类型,即truefalse,不可以是其它类型,否则会报错
  • 当代码块只有一条语句的时候,大括号{}可以省略,但建议不省略以提高代码的阅读性
1
2
3
4
5
6
7
8
if(条件表达式)
{
代码块1;
}
else
{
代码块2;
}
  • 当条件表达式为true时执行代码块1,为false时执行代码块2
  • 多分支
1
2
3
4
5
6
7
8
9
10
11
12
13
if(条件表达式1)
{
代码块1;
}
else if(条件表达式2)
{
代码块2;
}
...
else
{
代码块n;
}
  • 多分支可以没有else,此时若前面的所有条件表达式都为false,则这个分支没有执行入口
  • 嵌套分支
1
2
3
4
5
6
7
8
9
10
11
if(外层条件表达式)
{
if(内层条件表达式)
{
代码块1
}
else
{
代码块2
}
}
  • 代码规范:嵌套不要超过3层,否则代码可读性较差

switch case

1
2
3
4
5
6
7
8
switch(表达式)
{
case 常量1 : 语句块1; break;
case 常量2 : 语句块2; break;
...
case 常量n : 语句块n; break;
default : 默认语句块; break;
}
  • 程序会判断表达式是否和常量匹配,如果有匹配,则会以匹配的常量对应的代码块作为程序的入口,执行相应的语句块

  • 如果再执行完一个语句块之后,没有break语句,程序会继续执行下一条语句块,直到遇到break或者程序结束

  • 例如下面的程序,没有break。如果表达式输出的是常量2,那么程序将从语句块2开始执行,接着依次执行语句块3,语句块4,…,语句块n,默认语句块,最后结束

    1
    2
    3
    4
    5
    6
    7
    8
    switch(表达式)
    {
    case 常量1 : 语句块1;
    case 常量2 : 语句块2;
    ...
    case 常量n : 语句块n;
    default : 默认语句块;
    }
  • case后跟着的常量类型必须和表达式输出的数据类型一致,或者是可以自动转换成可以相互比较的类型,比如表达式输出的是char型,而case后的常量类型可以是int型和char

  • 表达式中返回值类型必须是byteshortintcharenumString,不可以是doublefloatboolean

  • 当表达式的返回值类型是double时,会报错

    1
    error: incompatible types: possible lossy conversion from double to int
  • 当表达式的返回值类型是boolean时,会报错

    1
    error: incompatible types: boolean cannot be converted to int
  • case子句中的值必须是常量或者常量表达式,而不能是变量

  • default子句是可选的,当没有匹配case时,会执行default,若也没有default子句则没有输出

switchif使用的选择

  • 如果判断的具体数值不多,而且复合byteshortintcharenumString这六种类型,建议使用switch
  • 其它情况,对区间的分支,以及对boolean类型的判断,应该使用if

循环结构

for循环

1
2
3
4
for (循环变量初始化; 循环条件; 循环变量迭代)
{
循环操作;
}
  • 循环操作可以有多条语句,如果只有一条语句,大括号{}可以省略,但建议不要省略
1
2
3
4
for (int i = 1; i <= 10; i++)
{
System.out.println(i);
}
  • 循环条件必须是一个返回boolean值的表达式

  • 循环变量初始化可以在循环外面定义,此时循环变量的作用域扩展到循环之外,可以在循环外使用循环变量

    1
    2
    3
    4
    5
    6
    int i = 1
    for ( ; i <= 10; i++)
    {
    System.out.println(i);
    }
    System.out.println(i);
  • 两个循环变量

    1
    for (int i = 0, j = 0; i + j < 10; i++, j += 2)
  • 循环变量迭代也可以写在循环操作中

    1
    2
    3
    4
    5
    for (int i = 1; i <= 10; )
    {
    System.out.println(i);
    i++;
    }
  • for的死循环,死循环可以和break语句配合使用,在开发中也很常见

    1
    for ( ; ; )

while循环

1
2
3
4
5
6
循环变量初始化;
while(循环条件)
{
循环体;
循环变量迭代;
}
1
2
3
4
5
6
int i = 1;
while(i <= 10)
{
System.out.println(i);
i++;
}
  • 循环条件一定是返回布尔值的一个表达式
  • while循环是先判断再执行语句

do...while循环

1
2
3
4
5
循环变量初始化;
do{
循环体;
循环变量迭代;
}while(循环条件)
  • do...while循环和while循环的区别在于do...while是先执行再判断

多重循环(重难点)

  • 循环的嵌套

    1
    2
    3
    4
    5
    6
    7
    for(int i = 0; i < 10; i++)
    {
    for(int j = 0; j < 10; j++)
    {
    System.out.println(i*j);
    }
    }
  • 代码规范:最多嵌套三层循环,否则代码可读性和执行效率较差

  • 实例——输出九九乘法表

    1
    2
    3
    4
    5
    6
    7
    8
    for (int i = 1; i <= 9; i++)
    {
    for (int j = 1; j <=i; j++)
    {
    System.out.print(i + "*" + j + "=" + i*j+"\t");
    }
    System.out.println(); //换行
    }
  • 实例——输出空心金字塔

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int rank = 5; //层数
    for (int i = 1; i <= rank; i++)
    {
    //输出空格
    for (int j = 1; j<=rank-i; j++)
    {
    System.out.print(" ");
    }
    //输出*号
    for (int j = 1; j <=2*i-1; j++)
    {
    if (j == 1 || j == 2*i-1 || i == rank)
    System.out.print("*");
    else
    System.out.print(" ");
    }
    //输出换行
    System.out.print("\n");
    }
    1
    2
    3
    4
    5
        *
    * *
    * *
    * *
    *********

跳转控制语句break、continue、return

break

  • break语句用于终止某个语句块的执行,一般使用在switch或者循环里面

  • break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块

  • 标签的基本使用

    1
    2
    3
    4
    5
    6
    7
    label1 : {
    label2 : {
    label3 : {
    break label2;
    }
    }
    }
  • break语句可以指明退出到哪一层

  • label1,label2,label3是标签,名字由程序员指定,符合标识符的命名规则

  • break后指定哪个标签,程序就跳出对应的代码块

  • 在实际开发中,尽量不要使用标签

  • 如果没有指定break,默认退出最近一层的循环

  • 字符串的比较,使用equals方法

    1
    2
    3
    System.out.println("林黛玉".equals("贾宝玉")); //false

    System.out.println("林黛玉".equals("林黛玉")); //true

continue

  • continue用于结束本次循环,进入下次循环
  • continue也可以和标签结合使用,使用方法和break一样

return

  • return使用在方法表示跳出所在的方法
  • 注意,如果teturn写在main方法,则会退出程序

数组

  • 数组可以存放多个同一类型的数据

  • 数组也是一种数据类型,是引用类型

  • 数组的定义

    1
    2
    int[] n = {1, 2, 3};
    double b[] = {1.0, 2.5, 3};
  • 可以通过中括号下标来访问数据元素

    Java中数组的下标是从0开始编号的

    1
    n[0], n[1], n[2];
  • 通过.length方法来得到数组的长度

    1
    arr.length
  • 使用new来创建数组

    1
    2
    // 创建一个int型数组,new5个int型大小的空间,用于存放int型数据
    int a[] = new int[5]
  • 数组的动态初始化

    1
    2
    3
    4
    5
    // 先声明数组
    int a[]; //或者 int[] a;
    // 再new空间
    a = new int[5];
    // 循环赋值
  • 数组的静态初始化

    1
    int a[] = {1, 2, 3, 4};
  • 数组使用注意事项

    1. 数组是多个相同类型的数据的组合,实现对数据的统一管理

    2. 数组中的元素可以是任何数据类型,包括基本数据类型和引用类型,但不能混合

    3. Java中数组创建后,如果没有赋值,是有默认值的

      int short byte long float double char boolean String
      0 0 0 0 0.0 0.0 \u0000 false null
    4. 使用数组的步骤

           graph LR
       声明数组 --> 开辟空间 --> 初始化 --> 使用数组
    5. 数组的下标是从0开始的

    6. 数组的下标必须在指定范围内使用,否则会报错

数组的赋值机制

  • 基本数据类型的赋值,这个值就是具体的数据,而且相互不影响

  • 数组在默认情况下的赋值操作,是引用传递,赋的值是地址

    1
    2
    3
    4
    5
    int[] arr1 = {1, 2, 3};
    int[] arr2 = arr1;
    // arr1将地址传递给arr2
    // 这样做arr1和arr2将指向同一块空间
    // 对两者其中任意一方的修改都是对同一块空间的修改

JVM对应的内存

  • 数组拷贝(内容复制)

    1
    2
    3
    4
    5
    6
    7
    8
    int[] arr1 = {1,2,3};
    // 重新开辟一块空间
    int[] arr2 = new int[arr1.length];
    // 使用循环进行拷贝
    for(int i = 0; i < arr1.length; i++)
    {
    arr2[i] = arr1[i];
    }
  • 数组练习

    将一个数字插入一个升序的数组,使得插入后的数组仍然升序

    1. 方法一:先查找再插入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    int insert = 23;
    int[] asc = {10, 12, 45, 90};

    // 查找插入位置
    location = asc.length;
    for(i = 0; i < asc.length; i++){
    if(asc[i] > insert){
    location = i;
    }
    }

    // 扩容数组并插入新元素
    int[] asc_new = new int[asc.length+1];
    for(int i = 0, j = 0; i < asc_new.length; ){
    if( i != index){
    asc_new[i] = arr[j];
    i++,j++;
    }else{
    asc_new[i] = insert;
    i++;
    }

    }
    1. 方法二:查找同时插入
      graph TD
    A["asc[i] <= insert"] --"True ==> [i]在插入位置左边"-->B["i == asc.length"]
    A --"False ==> [i]在插入位置右边"--> C["i == 0 || asc[i-1] <= insert"]
    B --"True ==> [i]为插入位置,元素插在末尾"-->D1["asc_new[i] = asc[i], asc_new[i+1] = insert;"]
    B --"False ==> [i]非插入位置,进行拷贝数组"-->E1["asc_new[i] = asc[i];"]
    C --"True ==> [i]为插入位置,元素插在开头或中间"--> F["asc_new[i] = insert,asc_new[i+1] = asc[i];"]
    C --"False ==> [i]非插入位置,进行拷贝数组"--> G["asc_new[i] = insert;"]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    int insert = 23;
    int[] asc = {10, 12, 45, 90};

    // 定义新的数组
    int[] asc_new = new int[asc.length+1];

    // 查找并插入
    for(int i = 0; i < asc.length; i++)
    {
    if(asc[i] <= insert)
    {
    // 如果i == asc.length,则元素应该插在末尾
    if(i == asc.length)
    {
    asc_new[i] = asc[i];
    asc_new[i+1] = insert;
    }
    // 否则i为非插入位置,则拷贝数组
    else
    {
    asc_new[i] = asc[i];
    }
    }
    else
    {
    // 如果i==0或者asc[i-1]<=insert,则i为插入位置
    // 注意要用短路或||
    if(i == 0 || asc[i-1] <= insert)
    {
    asc_new[i] = insert;
    asc_new[i+1] = asc[i];
    }
    // 否则i为非插入位置,则拷贝数组
    else
    {
    asc_new[i+1] = asc[i];
    }
    }
    }

二维数组

  • 二维数组的声明

    1
    2
    3
    int[][] arr = {{1,0,0,0},
    {0,1,0}, //并不需要每行每列的元素个数相同
    {0,0,1,0}};
  • 二维数组的遍历

    1
    2
    3
    4
    5
    6
    7
    8
    for(int i = 0; i < arr.length; i++)
    {
    for(int j = 0; j < arr[i].length; j++)
    {
    System.out.print(arr[i][j]+" ");
    }
    System.out.print("\n");
    }
  • 二位数组的初始化

    1
    2
    3
    4
    5
    6
    // 声明直接开辟空间
    int[][] arr = new int[2][3];

    // 先声明再开空间
    int arr[][];
    arr = new int[2][3];
  • 二维数组在计算机内存中的布局

    Java的二维数组在内存中的存储和C语言不一样,C的二维数组是一片连续的空间,而Java是地址套地址

JVM对应的内存 - 二维数组

  • 动态创建二维数组

    1
    2
    3
    4
    5
    // 目标:动态创建二维数组
    i=0: [1]
    i=1: [2 2]
    i=2: [3 3 3]
    // 三个一维数组,每一维的元素个数不一样
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int[][] arr = new int[3][];
    //这样声明之后会
    //arr[0]-->null
    //arr[1]-->null
    //arr[2]-->null

    for(int i = 0; i < arr.length; i++){
    // 给每个一维数组开辟空间
    // 如果没有给一维数组开辟空间,那么arr[i]就是null
    arr[i] = new int[i + 1];

    // 遍历一维数组,并给一维数组的每个元素赋值
    for(int j = 0; j < arr[i].length; j++){
    arr[i][j] = i + 1;
    }
    }
  • 二维数组使用细节和注意事项

    1. 一维数组的声明方式有

      1
      int[] x 或者 int x[]
    2. 二维数组的声明方式有

      1
      int[][] y 或者 int[] y[] 或者 int y[][]
    3. 二维数组实际上是由多个一维数组组成的,组成二维数组的各个一维数组的长度可以相同也可以不同,长度不相同的也被称为列数不等的二维数组

  • 二维数组练习1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int[] x,y[];
    // 判断下面各个语句是否能够通过编译
    // 说明:x是int类型一维数组,y是int类型的二维数组
    x[0] = y; //错误 int[][] -> int
    y[0] = x; //正确 int[] -> int[]
    y[0][0] = x; //错误 int[] -> int
    x[0][0] = y; //错误 x[0][0]错误
    y[0][0] = x[0]; //正确 int -> int
    x = y; //错误 int[][] -> int[]
  • 二维数组练习2

    1
    2
    3
    4
    // 下面数组定义正确的是
    a. String strs[] = {"a","b","c"}; //正确
    b. String strs[] = new String{"a","b","c"}; //错误
    c. String strs[] = new String[]{"a","b","c"}; //正确

面向对象

一个猫类的实例

  • 定义一个猫的类

    1
    2
    3
    4
    5
    6
    7
    // 自定义一个Cat类
    class Cat{
    // 属性,成员变量,Field(字段)
    String name;
    int age;
    String color;
    }
  • 创建对象

    1
    2
    3
    4
    5
    6
    // 先声明再创建
    Cat cat; //声明对象
    cat = new Cat();//创建对象

    // 直接创建对象
    Cat cat = new Cat();

类方法

  • 一个人类的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
String name;
int age;

// 方法(成员方法)
// 1.public 表示方法是公开的
// 2.void 表示方法是没有返回值的
// 3.speak() speak是方法名,()是形参列表
// 4.{...} 方法体
public void speak(){
System.out.println("我是一个好人");
}
}
1
2
3
4
// 先定义一个Person对象
Person p1 = new Person();
// 调用方法
p1.speak();
  • 一个计算类的实例
1
2
3
4
5
6
7
8
9
class Cal{

// 带参数和返回值的方法
// 计算两个数的和
public int getSum(int n1, int n2){
int res = n1 + n2;
return res;
}
}
1
2
3
4
// 先定义一个Cal的对象
Cal c1 = new Cal();
// 调用方法
int Res = c1.getSum(10,20);

方法的定义

  • 成员方法的定义
1
2
3
4
5
public [返回数据类型] [方法名] (参数列表)
{
[方法体];
return [返回值];
}
  1. 参数列表:表示成员方法输入
  2. 数据类型(返回值类型):表示成员方法输出,void表示没有返回值
  3. 方法体:表示为了实现某一功能的代码块
  4. return不是必须的

方法使用细节

  • 返回值可以为数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class RA{
    public int[] getSumAndSub(int n1, int n2){
    int[] reArr = new int[2];
    resArr[0] = n1 + n2;
    resArr[1] = n1 - n2;
    return resArr;
    }
    }

    RA ra = new RA();
    int[] res = a.getSumAndSub(1,4);
  • 参数类型可以为数组类型

    1
    2
    3
    4
    5
    6
    7
    8
    class PA{
    public void printArr(int[] formalArr){
    System.out.print(formalArr[0]);
    }
    }
    PA pa = new PA();
    int[] realArr = {1,2,3};
    pa.printArr(realArr);
  • 一个方法可以有0个参数或者多个参数,中间用逗号隔开

  • 参数类型可以为任意类型,包含基本类型或引用类型

  • 方法定义时的参数称为形式参数,简称形参

  • 方法调用时的参数称为实际参数,简称实参

  • 实参和形参的类型要一致或者兼容,个数和顺序必须一致

  • 实参传递给形参的是值,形参的任何改变都不会影响实参 <—— 课程0210方法传参机制1

  • 当方法的参数类型为数组类型时,实参传递给形参的是地址

    形参和实参存储着同一段地址,它们指向同一块堆空间 <—— 课程0211方法传参机制2

  • 方法体里面可以使用输入、输出、变量、运算、分支、循环 、方法调用等

    但是注意方法体里面不可以再定义方法!!!即:方法不能嵌套定义

  • 方法中调用方法

    1. 调用当前类的方法——直接调用
    2. 调用其它类的方法——通过对象调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Class_A
    {
    public void method_a()
    {
    System.out.println("A-a");
    }
    }
    class Class_B
    {
    public void method_b()
    {
    Class_A A = new Class_A();
    A.method_a();
    }
    }

    Class_B B = new Class_B();
    B.method_b();
    1
    2
    // 输出
    A-a
  • 方法的参数类型可以为对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 定义一个类
    class Person
    {
    String name;
    int age;
    }
    // 定义一个类方法
    class Test
    {
    public void changeAge(Person p)
    {
    p.age = 99;
    }
    }
    // 声明一个Person对象
    Person p = new Person();
    p.age = 1;
    // 声明一个Test对象
    Test t = new Test();
    // 调用方法修改Person对象的age属性
    t.changeAge(p);
    // 输出age为修改之后的值
    System.out.print(p.age); //99
  • 思考题——0212方法传递机制3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 定义一个类
    class Person{
    String name;
    int age;
    }
    // 定义一个类方法
    class Test{
    public void changeAge(Person p){
    p = null; //这里将对象p指向null
    }
    }
    // 声明一个Person对象
    Person p = new Person();
    p.age = 1;
    // 声明一个Test对象
    Test t = new Test();
    // 调用方法修改Person对象的age属性
    t.changeAge(p);
    // 输出age为修改之后的值
    System.out.print(p.age); //1

    在方法中将对象p指向null修改的只是p的指向,并没有对它地址所指向的空间中存储的内容做改变,所以输出的还是1

    JVM对应的内存 - 对象

递归的执行过程

  • 方法可以递归定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用递归定义一个计算阶乘的方法
class Math
{
public int factorial(int n)
{
if(n == 1)
return 1;
else
return n * factorial(n-1);
}
}

Math f = new Math();
int res = f.factorial(3);
  • 递归在内存中的细节

JVM对应的内存 - 递归

  • 递归的重要规则
    1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
    2. 方法的局部变量是独立的,不会相互影响,比如n变量
    3. 如果方法中使用的是引用类型变量(比如数组、对象),就会共享该引用类型的数据
    4. 递归必须有退出条件,并且向退出递归的条件逼近,否则就会无限递归,出现StackOverflowError
    5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕

递归求解迷宫问题

编写程序找到路径使得红色小人可以吃到绿色豆子

maze
  • 绘制地图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用二维数组来存储地图
int[][] map = new int[5][5];
for(int i = 0; i < 5; i++)
{
for(int j = 0; j < 5; j++)
{
if(i==0||i==4||j==0||j==4)
map[i][j] = 1;
else
map[i][j] = 0;
}
}
map[2][2] = 1;
map[3][2] = 1;
  • 显示当前地图
1
2
3
4
5
6
7
8
for(int i = 0; i < 5; i++)
{
for(int j = 0; j < 5; j++)
{
System.out.print(map[i][j] + " ");
}
System.out.println();
}
1
2
3
4
5
1 1 1 1 1
1 0 0 0 1
1 0 1 0 1
1 0 1 0 1
1 1 1 1 1
  • 递归找路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class T 
{
// findWay方法是递归定义的
// findWay方法用于递归找路
// map为当前地图,i和j对应当前坐标
// 返回true和false,true表示找到目标,false表示此方向不通
// 找路策略为 下->右->上->左
public boolean findWay(int[][] map, int i, int j)
{
if(map[3][3] == 2)
return true;
else
{
// 如果此位置不等于0,表示此路为墙(1)或者已走过(2)或者不通(3)
if(map[i][j] != 0)
return false;
else
{
//将其标记为2,用以表示已经走过的地点
map[i][j] = 2;
//按下右上左的顺序递归找路
//下
if(findWay(map, i + 1, j))
return true;
//右
if(findWay(map, i, j + 1))
return true;
//上
if(findWay(map, i - 1, j))
return true;
//左
if(findWay(map, i, j - 1))
return true;
//下右上左皆没有返回true,标记此位置为3,返回false
map[i][j] = 3;
return false;
}
}
}
}
1
2
3
// main中调用方法
T t1 = new T();
t1.findWay(map, 1, 1);
1
2
3
4
5
6
// 输出运行后的地图
1 1 1 1 1
1 2 2 2 1
1 3 1 2 1
1 3 1 2 1
1 1 1 1 1
  • 递归演示动画
maze

汉罗塔

  • 搬运n层汉罗塔的步骤树状图

    这里假定n为偶数去排列树状图

graph TD;
BA柱1至n层搬C柱;

B-->DA柱1至n-1层搬B柱;
B-->E(("A柱n层搬C柱"));
B-->E1B柱1至n-1层搬C柱;

D--"..."-->FA柱1至2层搬C柱;
D--"..."-->G(("A柱3层搬B柱"));
D--"..."-->G1C柱1至2层搬B柱;

F-->H["A柱1层搬B柱"];
F-->I(("A柱2层搬C柱"));
F-->H1["B柱1层搬C柱"];

G1-->G11["C柱1层搬A柱"];
G1-->G12(("C柱2层搬B柱"));
G1-->G13["A柱1层搬B柱"];

E1--"..."-->K1B柱1至2层搬A柱;
E1--"..."-->K2(("B柱3层搬C柱"));
E1--"..."-->K3A柱1至2层搬C柱;

K1-->K11["B柱1层搬C柱"];
K1-->K12(("B柱2层搬A柱"));
K1-->K13["C柱1层搬A柱"];

K3-->K31["A柱1层搬B柱"];
K3-->K32(("A柱2层搬C柱"));
K3-->K33["B柱1层搬C柱"];
  • 计算搬运n层汉罗塔的步骤数

    根据上面给出的树状图

    总步数=圆形叶子节点数+矩形叶子节点数圆形叶子结点数=1+2+22++2n2=2n11矩形叶子结点数=2n1总步数=2n1\begin{split} 总步数 &= 圆形叶子节点数 + 矩形叶子节点数 \\ 圆形叶子结点数 &= 1 + 2 + 2^2+\cdots+2^{n-2} = 2^{n-1} - 1 \\ 矩形叶子结点数 &= 2^{n-1}\\ &\Downarrow\\ 总步数 &= 2^n - 1 \end{split}

重载(OverLoad)介绍

  • 基本介绍

    Java中允许同一个类中,多个同名方法的存在,但是要求形参列表不一致

  • 重载举例

    System.out.println()方法

    Java8 API在线文档System类下的字段(Field)下的static PrintStream中可以找到很多的println()同名方法

    这些println()方法同名但是有着不同的参数列表,它们构成方法重载

    Type Method and Description
    void println()通过写入行分隔符字符串来终止当前行。
    void println(boolean x)打印一个布尔值,然后终止该行。
    void println(char x)打印一个字符,然后终止该行。
    void println(char[] x)打印一个字符数组,然后终止该行。
    void println(double x)打印一次,然后终止行。
    void println(float x)打印一个浮点数,然后终止该行。
    void println(int x)打印一个整数,然后终止行。
    void println(long x)打印很长时间,然后终止行。
    void println(Object x)打印一个对象,然后终止该行。
    void println(String x)打印一个字符串,然后终止行。
  • 重载的好处

    • 减轻了方法起名的麻烦
    • 减轻了方法记名的麻烦
  • 重载的注意细节

    • 方法名必须相同

    • 参数列表必须不同(参数类型、个数、顺序至少有一样不同)

    • 参数名无要求

    • 返回类型无要求

    • 如果两个方法名和参数列表一样,那么会报错方法重定义

      1
      error: method ... is already defined in class ...

方法重载案例

使用方法重载实现下面四个方法

1
2
3
4
calculate(int a, int b);
calculate(int a, double b);
calculate(double a, int b);
calculate(double a, double b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 实现
class MyCalculator{
//int + int
public int calculate(int a, int b){
System.out.println("int + int");
return a + b;
}

//int + double
public double calculate(int a, double b){
System.out.println("int + double");
return a + b;
}

//double + int
public double calculate(double a, int b){
System.out.println("double + int");
return a + b;
}

//double + double
public double calculate(double a, double b){
System.out.println("double + double");
return a + b;
}
}

MyCalculator mycal = new MyCalculator();
mycal.calculate(1,1); //输出int + int
mycal.calculate(1,1.0); //输出int + double
mycal.calculate(1.0,1); //输出double + int
mycal.calculate(1.0,1.0);//输出double + double

可变参数

  • 基本概念

    Java允许将同一个类中多个同名同功能参数个数不同的方法封装成一个方法

  • 基本语法

    1
    2
    3
    [访问修饰符] [返回类型] [方法名]([数据类型]... [参数名]){
    ...
    }
  • 实例——计算2,3,4…,n个数的和

    使用方法重载将会显得很复杂

    使用可变参数,代码可以被优化得很简洁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //int...表示可变参数,类型是int,即可以接收多个int型参数(0个到任意多个)
    //使用可变参数时,可以当作数组来使用,即nums当作数组
    class GetSum{
    public int sum(int... nums){
    int res = 0;
    for(int i = 0; i < nums.length; i++){
    res += nums[i];
    }
    return res;
    }
    }

    GetSum getsum = new GetSum();
    int num = getsum.sum(1,2,3,4,5,6);
    System.out.println(num); //21
  • 注意事项和使用细节

    • 可变参数的实参个数可以为0个或者任意多个

    • 可变参数的实参可以为数组

      1
      2
      int[] arr = {1,2,3,4};
      int num = getsum.sum(arr); //10
    • 可变参数的本质就是数组

    • 可变参数可以和普通数组的参数一起放在形参列表,但必须保证可变参数在最后

      1
      2
      3
      public int varparmethod(String str, int... nums){
      ...
      }
    • 一个形参列表最多只能出现一个可变参数

      当参数列表需要多个可变参数时,可以考虑采用数组类型

      1
      2
      // 多个可变参数报错
      error: ')' expected

作用域(scope)

  • 在Java编程中,主要的变量就是属性(成员变量)和局部变量
  • 我们说的局部变量一般是指在成员方法中定义的变量
  • Java中作用域的分类
    • 全局变量:也就是属性,作用域为整个类体
    • 局部变量:也就是除了属性外的其它变量,作用域为定义它的代码块中
  • 全局变量可以不赋值,直接使用,因为有默认值
  • 局部变量必须赋值后才能使用,因为没有默认值
  • 属性和局部变量可以重名,访问时遵循就近原则
  • 在同一个作用域内,两个局部变量不能重名(比如在同一个成员方法中)
  • 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡
  • 局部变量生命周期较短,便随着它的代码块的执行而创建,伴随着代码块的结束而死亡
  • 作用域的范围不同
    • 全局变量/属性:可以被本类使用,或者其它类通过对象调用
    • 局部变量:只能在本类对应的方法中使用
  • 修饰符不同
    • 全局变量可以加修饰符
    • 局部变量不可以加修饰符

构造方法/构造器(Constructor)

创建对象的时候直接对对象进行初始化操作,同c++中类的构造函数

  • 基本语法

    1
    2
    3
    [修饰符] 方法名(形参列表){
    方法体;
    }
  • 说明

    • 构造方法的修饰符可以默认,也可以是public/protected/private
    • 构造方法没有返回值,也不能加void
    • 构造方法名必须和类名一样
    • 构造方法的调用由系统完成
    • 在创建对象时,系统会自动调用该类的构造方法完成对对象的初始化
  • 实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Person{
    String name;
    int age;
    //构造方法
    // 1. 构造方法没有返回值,也不能些void
    // 2. 构造方法的名字和类名一样
    public Person(String pName, int pAge){
    name = pName;
    age = pAge;
    System.out.println("构造函数调用")
    }
    }

    Person p1; //声明时不调用构造方法
    p1 = new Person("Smith", 80); //new空间时调用
  • 注意事项和细节

    1. 一个类可以定义多个不同的构造方法,即构造方法重载

    2. 构造方法名和类名要相同

    3. 构造方法没有返回值,也不能指定void

    4. 构造方法在对象new空间时被调用

    5. 在new空间时系统自动调用该类的构造方法

    6. 如果程序员没有定义构造方法,系统会自动给类生成一个默认的无参的构造方法,例如:

      1
      Person(){}
    7. 使用javap指令反编译.class文件可以看到默认的构造器

      • 测试代码
      1
      2
      3
      4
      5
      6
      7
      8
      public class Hello{
      public static void main(String[] args){}
      }

      class Person{
      String name;
      int age;
      }
      • 在TERMINAL中执行

        1
        javac Hello.java

        执行之后目录中将多出两个文件Hello.classPerson.class

      • Person.class进行反编译

        1
        javap Person.class
        1
        2
        3
        4
        5
        6
        7
        c:\Users\xiaophai\Desktop\Java>javap Person.class
        Compiled from "Hello.java"
        class Person {
        java.lang.String name;
        int age;
        Person();
        }
      • 可以看到在反编译的结果中出现了默认的构造方法

    8. 一旦定义了自己的构造方法,默认的构造方法就被覆盖了

对象创建流程(内存中的细节)

0244_韩顺平Java_对象创建流程详解

this关键字

  • 当构造方法的形参名和类的属性名相同时,在构造方法中使用this关键词来指定类属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person{
    String name;
    int age;

    public Person(String name, int age){
    //this.name和this.age是当前对象的属性
    //name和age是局部变量
    this.name = name;
    this.age = age;
    }
    }
  • this的本质(在内存中)

    0247_韩顺平Java_this本质

  • this使用细节和注意事项

    1. this关键字可以用来访问本类的属性、方法、构造器

    2. this用于区分当前类的属性和局部变量

    3. 访问成员方法的语法:this.方法名(参数列表);

      两种调用本类方法的形式有区别,区别将在类的继承中讲述

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Test{
      public void f1(){
      System.out.println("调用f1()方法");
      }

      public void f2(){
      //调用本类中的方法
      //第一种直接调用
      f1();
      //第二种用this调用
      this.f1();
      }
      }
    4. 访问构造器语法:this(参数列表);

      注意只能在构造器中使用this()调用另外一个重载的构造器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Test{
      public Test(){
      //在构造器中调用重载的构造器
      //this()语句必须放在第一条语句
      //同一个构造器中不能有两个this语句,否则会违反上面一条
      this("Jack");
      System.out.println("调用构造器");
      }
      public Test(String name){
      System.out.println("Hello," + name);
      }
      }

      递归调用构造器会报错

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Test{
      public Test(){
      //递归调用构造器报错
      this();
      ...
      }
      ...
      }

      //错误信息
      Recursive constructor invocation

      在成员方法中使用this访问构造方法会报错

      1
      2
      3
      4
      5
      6
      class Test{
      public void fun(){
      //成员方法中使用this调用构造函数
      this();
      }
      }
      1
      2
      //报错信息
      error: call to this must be first statement in constructor

      在成员方法中使用this调用构造方法会报错

      1
      2
      3
      4
      5
      6
      class Test{
      public void fun(){
      //成员方法中使用this调用构造函数
      this.Test();
      }
      }
      1
      2
      3
      4
      5
      //报错信息
      error: cannot find symbol
      this.Person();
      ^
      symbol: method Person()
    5. this不能在类定义的外部使用,只能在类定义的方法中使用

IDEA的使用

教育邮箱

  • 申请理工大的教育邮箱

如果你还没有申请理工大的教育邮箱, 可以在智慧理工大>办事大厅>自主邮箱申请服务中申请.

  • 登录理工大的教育邮箱

进入邮箱的方式, 可以从智慧理工大首页>我的信息>学校邮箱直接进入, 不需要输入用户名和密码

也可以通过网址http://mail.whut.edu.cn/输入用户名和密码登录邮箱.

  • JetBrains Products for Learning

JetBrains Products for Learning中点击apply, 填写表单进行申请, 成功后Jetbrain会向你的教育邮箱中发送一封右键, 跟随邮件中的指示将License绑定到你的Jetbrain账户中即可. 此后在account\license中登录你的Jetbrain账户, 可以查看你的License信息.

  • 激活Jetbrains产品

下载最新版的IEAD, Pycharm, 选择Get license from: JB Account, 跟随提示登录Jetbrain账户即可完成激活

  • 更新License

license的有效期只有一年, 在license到期的前一周开始以及到期后, account\license中会出现 Renew my Education Pack 的提示, 点击它申请更新 License. 随后填写一些简单的信息, JetBrains 会向你的教育邮箱发送一份邮件, 点击其中的链接并接受条款即可.
Renew my Education Pack

常用快捷键

File->Settings->Keymap中修改快捷键

命令 快捷键
运行 默认Shift+F10
修改Ctrl+Alt+N
保存 Ctrl+S
注释 / 取消注释 Ctrl+/ / Ctrl+/
缩进 / 取消缩进 Tab / Shift+Tab
查找 / 替换 Ctrl+F/R
删除当前行 Ctrl+Y
复制当前行 Ctrl+D
*补全代码 Alt+/
快速对齐 Ctrl+Alt+L
快速定位方法 Ctrl+B
显示类层级 Ctrl+H
*自动分配变量名 .var
快速生成构造方法 笔记本键盘Alt+Fn+F12
标准键盘 Alt+Insert
鼠标右键 Generate
自动导入包 Alt+Enter
需要在File->Settings->Editor->General->Auto Import->Java中勾选
✓▢ Add unambiguous imports on the fly
✓▢ Optimize imports on the fly
查看JDK源码 将光标放置类或者方法上,Ctrl+B
右键Go To > Declaration or Usages

实用功能

自动代码补全

  • 当你敲了一些字母后编译器会自动匹配你接下来可能的输入,此时使用上下方向键确定单词,接着使用左手小指按一下Tab键自动补全代码

自动匹配

  • 而当你移动了光标后,编译器的自动匹配框没有了,此时可以使用Alt+/快捷键来自动补全代码

移动光标自动匹配消失

自动分配变量名

1
2
3
new Person().var+[Enter]
//自动分配变量名,并且结尾分号
Person person = new Person();

参数提示

在File->Settings->Editor->Inlay Hints->Parameter names->Java中勾选

✓▢ Parameters with names that are contained in the method name

image-20220920113827618

以上灰色的部分不是实际的代码,只是编译器给的提示

常用模板

  • 查看和添加模板

File->Settings->Live Templates->Java查看以及添加模板

  • fori+Enter

    只能在方法中使用,在类内无效

    1
    2
    3
    for (int i = 0; i < ; i++) {

    }
  • sout+Enter

    只能在方法中使用,在类内无效

    1
    System.out.println();
  • main+Enter

    只能在类内使用,在方法中无效

    1
    2
    3
    public static void main(String[] args) {

    }

  • 包的三大作用

    1. 区分相同名字的类

    2. 当类很多时,可以很好的分组管理类

    3. 控制访问范围

  • 包的基本语法

    1
    package com.hspedu;
    1. package是关键字,表示打包
    2. com.hspedu是包名
  • 包的本质

    包的本质实际上就是创建不同的文件夹来保存类文件

包快速入门示例

创建两个包,并创建同名类Dog

  • 新建Java项目

  • 右键单击src -> new -> package

  • 输入包名com.xiaophai,对应的src文件夹目录下出现两级目录\com\xiaophai

  • 再次新建包com.zhangwx,对应的src文件夹目录下出现两级目录\com\zhangwx

  • 右键单击\com\xiaophai -> new -> Java Class

    1
    2
    3
    4
    5
    package com.xiaophai;

    public class Dog {

    }
  • 右键单击\com\zhangwx -> new -> Java Class

    1
    2
    3
    4
    5
    package com.zhangwx;

    public class Dog {

    }

使用Dog类

  • 创建包com.use

  • 在com.use包下创建Test类

  • 在Test类中键盘输入main方法

    1
    2
    3
    4
    5
    6
    7
    package com.use;

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

    }
    }
  • 创建com.xiaophai包下的Dog类对象

    • 导入Dog类

      1
      import com.xiaophai.Dog;
    • 在main方法中创建Dog对象并输出对象

      1
      2
      3
      4
      5
      6
      public class Test {
      public static void main(String[] args){
      Dog dog1 = new Dog();
      System.out.println(dog1);
      }
      }
      1
      2
      // Terminal输出
      com.xiaophai.Dog@1b6d3586

      上面输出的@后面的字符串1b6d3586,是对应的Dog类的Hash Code的十六进制写法

  • 同时创建com.xiaophai包和com.zhangwx包下的Dog类对象

    • 此时不能同时导入同名Dog类,否则会报错

      1
      2
      import com.xiaophai.Dog;
      import com.zhangwx.Dog;
      1
      2
      // 提示错误
      'com.xiaophai.Dog' is already defined in a single-type import
      1
      2
      // 运行报错
      java: a type with the same simple name is already defined by the single-type-import of com.xiaophai.Dog
    • 此时可以使用在类前加包名的方式,来区分不同包下的同名类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Test {
      public static void main(String[] args){
      com.xiaophai.Dog dog1 = new com.xiaophai.Dog();
      com.zhangwx.Dog dog2 = new com.zhangwx.Dog();

      System.out.println(dog1);
      System.out.println(dog2);
      }
      }
      1
      2
      3
      // Terminal输出
      com.xiaophai.Dog@1b6d3586
      com.zhangwx.Dog@4554617c

包的命名

命名规则
包的命名规则

  1. 只能包含数字、字母、下划线、英文句号
  2. 不能以数字开头
  3. 不能是关键字或保留字
1
2
3
demo.class.one  //错误,class是关键字
demo.12two //错误,12a以数字开头
demo.ab12.three //正确

命名规范
一般是com.公司名.项目名.业务模块

1
2
3
4
// 举例
com.sina.crm.user //新浪crm项目下的用户模块
com.sina.crm.order //新浪crm项目下的订单模块
com.sina.crm.utils //新浪crm项目下的工具类模块

常用的包

  • java.lang.*

    lang包是基本包,默认引入,不需要再额外引入。

    lang包中包含IntegerMathObjectstringSystem等类,在使用这些包的时候直接使用,不需要import

  • java.util.*

    util包,系统提供的工具类包,使用其中的类需要引入,比如使用其中的Scanner类需要添加import代码

    1
    import java.util.Scanner; // 导入java.util包下的Scanner类
  • java.net.*

    net包是网络包,提供网络开发所需要的方法

  • java.awt.*

    awt包是用java做图形界面开发的包

包的使用细节

引入包

1
import 包名;
  • 引入一个包的目的是要使用该包下的类

    • 当包名具体到类时,表示只引入该类

      1
      2
      // 比如这样就只引入一个Scanner类
      import java.util.Scanner;
    • 当包名不具体指定类时,表示引入该包下的全部类

      1
      2
      // 比如这样将引入util包下的所有类
      import java.util.*;
  • 建议不要使用.*的方式导入包中的全部类,而是需要什么类就导入对应的类

  • 设置自动导入

    在File->Settings->Editor->General->Auto Import->Java中勾选
    ✓▢ Add unambiguous imports on the fly

示例:Arrays类的使用

使用系统提供的Arrays完成数组排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入Arrays类
import java.util.Arrays;

public class Test {
public static void main(String[] args){
// 使用系统提供的Arrays类中的sort方法完成数组排序
int[] arr = {3,1,4,-5,9,2,6};
Arrays.sort(arr);
// 输出排序后的结果
for(int i=0; i < arr.length; i++){
System.out.print(arr[i] + "\t");
}
}
}
1
2
// Terminal输出
-5 1 2 3 4 6 9

Package使用

  • package的作用时声明当前类所在的包,必须要放在类的最上面一个类中最多只有一个package语句
  • import指令需放在package的下面,并在类定义的前面,可以有多个import并且没有顺序要求

访问修饰符

java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限

  1. 公开级别:public修饰,对外公开
  2. 受保护级别:protected修饰,对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包的类公开
  4. 私有级别:private修饰,只有类本身可以访问,不对外公开
访问级别 访问控制修饰符 本类 同包 不同包子类 不同包
公开 public
受保护 protected
默认 无修饰符
私有 private

注意事项

  1. 修饰符可以用来修饰类以及类中的属性,成员方法

  2. 只有默认和public才能修饰类,并且遵循上述访问权限的特点

    1
    2
    3
    4
    5
    6
    7
    8
    public class A1{}
    //Class 'A1' is public, should be declared in a file named 'A1.java'
    protected class A2{}
    //Modifier 'protected' not allowed here
    class A3{}
    //无提示
    private class A4{}
    //Modifier 'private' not allowed here
  3. 成员方法的访问规则和属性完全一样

示例:四种访问修饰符

  • 本类中的方法可以访问四种修饰符修饰的类内成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// com\use\A.java
package com.use;

public class A{
// 四种属性,分别使用不同的访问修饰符
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;

// 本类中的方法可以访问四种修饰符修饰的类内成员变量
public void m1(){
System.out.println(this.n1);
System.out.println(this.n2);
System.out.println(this.n3);
System.out.println(this.n4);
}
}
  • 不同类的方法,可以访问同一个包下不同类对象的public、protected和默认修饰的属性

    不能访问private修饰的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// com\use\B.java
package com.use;

public class B {
public void m2(){
A a = new A();
// 不同类的方法,可以访问同一个包下,类对象的public、protected和默认修饰的属性

System.out.println(a.n1); //public
System.out.println(a.n2); //protected
System.out.println(a.n3); //默认

// 不能访问private修饰的属性
//System.out.println(a.n4); //private
//'n4' has private access in 'com.use.A'
}
}
  • 在main函数中调用方法
1
2
3
4
5
6
7
8
9
10
11
// com\use\Test.java
package com.use;

public class Test{
public static void main(String[] args) {
A a1 = new A();
a1.m1();
B b1 = new B();
b1.m2();
}
}
  • 不同包下的类的方法,只能访问不同包下的类的public修饰的属性
    不能访问protected、默认、private修饰的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// com\xiaophai\Test.java
package com.xiaophai;

import com.use.A;

public class Test {
public static void main(String[] args) {
A a1 = new A();
// 不同包,只能访问不同包下的类对象的public修饰的属性
// 不能访问protected、默认、private修饰的属性
System.out.println(a1.n1);

// System.out.println(a1.n2);
//'n2' has protected access in 'com.use.A'

// System.out.println(a1.n3);
//'n3' is not public in 'com.use.A'. Cannot be accessed from outside package

// System.out.println(a1.n4);
//'n4' has private access in 'com.use.A'
}
}

类的访问修饰符

  • 只有默认和public才能修饰类,protected和private都不可以用来修饰类
1
2
3
4
5
6
7
8
public class A1{}
//Class 'A1' is public, should be declared in a file named 'A1.java'
protected class A2{}
//Modifier 'protected' not allowed here
class A3{}
//无提示
private class A4{}
//Modifier 'private' not allowed here
  • 对于public,一个.java文件只能定义一个public修饰的类,并且类名必须与文件名相同,否则会报错
1
2
3
4
5
6
//A.java

public class A{ }

public class B{ }
//Class 'B' is public, should be declared in a file named 'B.java'
  • public修饰的类可以被不在同一个包的类访问到

    默认修饰的类只能被同一个包中的类访问到

访问级别 访问控制修饰符 本类 同包 不同包子类 不同包
公开 public
默认 无修饰符
  • 在不同包中调用public类时,需要在包前面加上类所在的包名,或者使用import导入类
1
2
3
4
5
6
//com.Package1.A
package com.Package1;

public class A {}

class B{}
1
2
3
4
5
6
7
8
9
10
11
12
13
//com.Package2.Main
package com.Package2;

//import com.Package1.A;

public class Main {
public static void main(String[] args) {
com.Package1.A a = new com.Package1.A();
//默认修饰的类在外包中不可以被访问
//com.Package1.B
//'com.Package1.B' is not public in 'com.Package1'. Cannot be accessed from outside package
}
}

面向对象编程三大特征

★封装★encapsulation

★继承★inheritance

★多态★polymorphic

★封装★

封装(encapsulation)就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只通过被授权的操作(方法),才能对数据进行操作

  • 封装的好处
    1. 隐藏实现细节
    2. 可以对数据进行验证,保证安全合理
  • 封装的实现步骤
    1. 将属性私有化private(类外不能直接访问)
    2. 提供一个public的set方法,用于对私有属性进行各种操作
    3. 提供一个public的get方法,用于获取私有属性的值

示例:封装

编写一个Person的类,要求不能随便查看人的年龄、工资等隐私,并对设置的年龄进行合理的验证。年龄合理(1-120)就设置,否则给默认年龄(18),工资不能直接查看,name的长度在2-6字符之间

快捷键Alt+Fn+F12 Getter and Setter,快速设置set和get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.xiaophai;

public class Encapsulation {
public static void main(String[] args) {
Person p = new Person();
p.setName("zhang");
p.setAge(22);
p.setSalary(30000);
p.showInfo();

}
}

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

// 快捷键Alt+Fn+F12 Getter and Setter

//获取名字
public String getName() {
return name;
}

//设置名字
public void setName(String name) {
if(name.length >= 2 && name.length <= 6){
this.name = name;
}else{
System.out.println("The name you input is improper")
}

}

//获取年龄
public int getAge() {
return age;
}

//设置年龄
public void setAge(int age) {
if(age >= 1 && age <= 120) {
this.age = age;
}else{
System.out.println("The age you set is improper");
this.age = 18;
}
}

//获取薪水
public double getSalary() {
return salary;
}

//设置薪水
public void setSalary(double salary) {
this.salary = salary;
}

//输出所有信息
public void showInfo(){
System.out.println("Name:"+name+"\tAge:"+age+"\tSalary:"+salary);
}

}

封装与构造器

  • 使用构造器直接对参数赋值可以绕过Set方法,从而不会对输入信息进行判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.xiaophai;

public class Encapsulation {
public static void main(String[] args) {
//直接使用构造器指定属性
Person smith = new Person(name:"Smith", age:20, salary:50000);
smith.showInfo();
}
}

class Person{
...

// 快捷键Alt+Fn+F12 Constructor

public Person(String name, int age, double salary){
this.name = name;
this.age = age;
this.salary = salary;
}

...
}



  • 在构造器中使用Set方法对参数进行初始化可以避免这种情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.xiaophai;

public class Encapsulation {
public static void main(String[] args) {
//直接使用构造器指定属性
Person smith = new Person("Smith", 20, 50000);
smith.showInfo();
}
}

class Person{
...

// 快捷键Alt+Fn+F12 Constructor

public Person(String name, int age, double salary){
setName(name);
setAge(age);
setSalary(salary);
}

...
}

★继承★

  • 继承可以解决代码复用性的问题,让编程更加靠近人类思维

    当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends关键词来声明继承父类即可

  • 继承的基本语法

    1
    2
    3
    class 子类 extends 父类{
    ...
    }
    1. 子类自动拥有父类定义的属性和方法
    2. 父类又叫 超类,基类
    3. 子类又叫派生类
  • 继承给编程带来的好处

    1. 代码的复用性提高了
    2. 代码的扩展性和维护性提高了

继承示例

  • 父类Student
1
2
3
4
5
6
7
8
9
10
11
12
13
// Student.java
package com.Extend_;

public class Student {
//共有属性
public String name;
public int age;

//共有方法
public void showInfo(){
System.out.println("Name:" + name + " Age:" + age);
}
}
  • 子类Pupil
1
2
3
4
5
6
7
8
9
// Pupil.java
package com.Extend_;

// Pupil 继承 Student
public class Pupil extends Student{
public void introduce(){
System.out.println("I am " + name + ", a pupil.");
}
}
  • 子类Undergraduate
1
2
3
4
5
6
7
8
9
// Undergraduate.java
package com.Extend_;

// Undergraduate 继承 Student
public class Undergraduate extends Student{
public void introduce(){
System.out.println("I am " + name + ", an undergraduate.");
}
}
  • main方法调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TestExtends.java
package com.Extend_;

public class TestExtends {
public static void main(String[] args) {
Pupil pupil = new Pupil();
pupil.name = "小明";
pupil.age = 8;
pupil.introduce();
pupil.showInfo();

Undergraduate undergraduate = new Undergraduate();
undergraduate.name = "大明";
undergraduate.age = 18;
undergraduate.introduce();
undergraduate.showInfo();
}
}

构造器不能被继承

构造器为什么不能被继承?

这是由于构造器的特殊规定决定的,构造器的定义和普通方法相比:

  • 首先构造器不需要返回类型
  • 其次构造器和类名相同

如果构造器可以被子类继承,其具备的地位有两种可能:

  • 一是作为子类的构造器
  • 二是作为子类对象的普通方法

但是,如果作为子类的构造方法,其和子类名不一致,违背了构造方法的规定;作为子类对象的普通方法,则其没有返回值,违背了普通方法的规定。

综上,构造器不能被子类继承。

继承与访问标识符

子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类中直接访问,但是私有的属性和方法不能在子类中直接访问,要通过父类提供的public的方法来访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Superclass.java
package com.Extend2;

public class Superclass {
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;

// 公共方法用于访问私有属性或私有方法
public int getN4(){
return n4;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Subclass.java
package com.Extend2;

public class Subclass extends Superclass{
public void Test(){
// 父类非私有的属性和方法可以在子类的方法中直接访问
System.out.println(n1); //public
System.out.println(n2); //protected
System.out.println(n3); //默认

// 父类私有的属性和方法在子类的方法中不能被直接访问
//System.out.println(n4); //private
//'n4' has private access in 'com.Extend2.Superclass'

// 要通过父类提供的public方法来访问
System.out.println(getN4());
}
}

继承与构造器

  • 子类在创建对象时,必须先调用父类的构造器,完成父类的初始化

创建子类对象时,先调用父类的构造器,再调用子类的构造器

1
2
3
4
5
6
7
8
// Superclass.java
package com.Extend2;

public class Superclass {
public Superclass(){
System.out.println("Superclass Constructor");
}
}
1
2
3
4
5
6
7
8
// Subclass.java
package com.Extend2;

public class Subclass extends Superclass{
public Subclass(){
System.out.println("Subclass Constructor");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// Main.java
package com.Extend2;

public class Main {
public static void main(String[] args) {
Subclass sub = new Subclass();
}
}

// 输出
Superclass Constructor //先调用父类构造器
Subclass Constructor //再调用子类构造器
  • 当创建子类对象时,不管使用子类的哪个(重载)构造器,默认情况下都会调用父类的无参构造器

    如果父类没有提供无参构造器(自定义了有参构造器,默认无参构造器被覆盖),则必须在子类的构造器中用super方法去指定参数,以确定使用哪个父类的构造器完成对父类的初始化工作,否则编译不会通过

1
2
3
4
5
6
7
8
// Superclass.java
package com.Extend2;

public class Superclass {
public Superclass(String name, int age){
System.out.println("Superclass Constructor");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Subclass.java
package com.Extend2;

public class Subclass extends Superclass{

public Subclass(){
super("xiaophai", 18); //子类构造器中要用super方法指明父类构造器的参数
System.out.println("Subclass No Arguments Constructor");
}
public Subclass(String name, int age){
super("xiaophai", 18); //每个重载的构造器都需要设置,否则报错
System.out.println("Subclass Constructor With Arguments");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Main.java
package com.Extend2;

public class Main {
public static void main(String[] args) {
Subclass sub1 = new Subclass(); //调用子类无参构造器
Subclass sub2 = new Subclass("zhangwx",22); //调用父类无参构造器
}
}

// 第一个对象sub1输出
Superclass Constructor with Arguments//父类有参构造器
Subclass No Arguments Constructor //子类无参构造器
// 第二个对象sub2输出
Superclass Constructor with Arguments//父类有参构造器
Subclass Constructor With Arguments //子类有参构造器
  • 使用super方法,来指定调用父类的某个重载构造器
1
super(参数列表);
  • super(参数列表)只能在构造器中使用
  • super(参数列表)在使用时,必须放在子类构造器的第一行
  • super(参数列表)this(参数列表)都必须放在构造器的第一行
    因此这两个方法不能同时使用

this(参数列表)用于在构造器中调用重载的构造器

继承与Object根父类

  • Java所有的类都是Object类的子类,Object是所有类的父类
  • 使用Ctrl+H可以查看类的层级关系(class hierarchy)
  • 父类构造器的调用不限于直接父类,将一直追根溯源到Object类(顶级父类)

继承与单继承

  • 子类最多只能继承一个父类(指直接继承),即Java中的继承是单继承

  • 如果需要让A类同时继承B类和C类,可以让A类继承B类,并让B类继承C类

  • 不能滥用继承,子类和父类必须满足is-a的逻辑关系

    Person is a Music?✘
    Person与Music不满足is-a的关系,所以不能用Person继承Music

    Cat is an Animal?✔
    Cat与Animal满足is-a的关系,所以可以用Cat来继承Animal

  • 一个子类只能有一个父类,但一个父类可以有多个子类

继承的本质(内存)

【P294 0293_韩顺平Java_继承本质详解】

  • 当在子类中访问一个属性(或方法)时
    1. 首先看子类是否有该属性
      • 如果子类有该属性,并且可以访问,则返回信息
      • 如果子类有该属性,但是不可以访问 ,则报错
    2. 如果子类没有该属性,则向上一级查找父类是否有该属性
      • 如果父类有该属性,并且可以访问,则返回信息
      • 如果父类有该属性,但是不可以访问 ,则报错
    3. 如果父类也没有该属性,则继续向上一级重复上面的过程,查找父类的父类是否有该属性,直到Object类
graph TB
  a--有-->b--可以访问-->b1[返回该属性值];
  b--不可访问-->b2[报错];
  a--无-->c;
  c--有-->d;
  d--可以访问-->f;
  d--不可访问-->g;

  c--无-->e;
  e--"......"-->h[往上查找直到Object类];

继承课堂练习

  1. main中:B b = new B();会输出什么?

    1
    2
    3
    4
    5
    6
    7
    8
    class A{
    A(){System.out.println("a");}
    A(String name){System.out.println("a name");}
    }
    class B extends A{
    B(){this("abc");System.out.println("b");}
    B(String name){System.out.println("b name");}
    }

    注意B类中,构造器B(String name){...}隐含了第一句的super();相当于

    1
    B(String name){super();System.out.println("b name");}

    而无参构造器B(){...}中使用了this(...),隐含的super()会失效

    1
    2
    3
    4
    //输出
    a
    b name
    b
  2. main方法中:C c = new C();会输出什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class A{
    public A(){System.out.println("A类无参构造器");}
    }

    class B extends A{
    public B(){System.out.println("B类无参构造器");}
    public B(String name){System.out.println("B类有参构造器: " + name);}
    }

    class C extends B{
    public C(){this("Hello");System.out.println("C类无参构造器");}
    public C(String name){super("Yahaha");System.out.println("C类有参构造器: " + name);}
    }
    1
    2
    3
    4
    5
    // 输出
    A类无参构造器
    B类有参构造器: Yahaha
    C类有参构造器: Hello
    C类无参构造器

super关键字

  • super关键字类似于this,不同点在于super用于访问父类的属性方法构造器,而this用于访问本类的属性方法构造器

  • 在子类中使用super关键字可以指定访问父类的非private属性

1
super.父类属性;
  • 在子类中使用super关键字可以指定访问父类的非private方法
1
super.父类方法(参数列表);
  • 访问父类的构造器
1
2
//只能放在构造器的第一句
super(参数列表);
  • 在子类的构造器中使用super(参数列表),来指定调用父类的构造器

    • super(参数列表)只能在构造器中使用

    • super(参数列表)在使用时,必须放在构造器的第一行

    • super(参数列表)this(参数列表)都必须放在构造器的第一行
      因此这两个方法不能同时使用

    • 从另一个方面来看,使用this必然会调用其它构造器,其它构造器必然存在super,所以如果this和super可以同时出现在构造器中会导致一个构造器中出现多个super的情况,这会导致重复调用父类的构造器,编译不会通过

  • this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。

直接访问、this、super的区别

  • 直接访问会先在局部代码块中查找,如果有则调用
    没有则在本类中查找,如果有则调用
    没有则向上一级,直到Object

    直接访问是就近原则,当局部变量和属性/成员重名时,直接访问会优先调用局部变量

  • this会先在本类查找,如果有则调用
    没有则在父类中查找,如果有则调用
    没有则继续向上一级查找,直到Object

  • super会跳过本类,直接在父类中查找,如果有则调用
    没有则在父类的父类中查找,如果有则调用
    没有则继续向上一级查找,直到Object

  • 三种调用方法,如果在查找过程中,找到但不能访问,则提示属性/方法不可以被访问
    如果直到Object都没有查找到,则提示Cannot resolve symbol xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main中调用B类的say方法

class A{
public int p = 1;
}

class B extends A{
public int p = 2;

public void say(){
int p = 3;
System.out.println(p); //3 局部变量
System.out.println(this.p); //2 本类属性
System.out.println(super.p); //1 父类属性
}
}

  • 使用super(参数列表)调用父类构造器的好处:

    分工明确,父类属性由父类初始化,子类的属性由子类初始化

  • 当子类中有和父类中重名的成员(属性和方法)时,为了访问父类的成员,必须通过super方法

    如果没有重名,则使用super、this、直接访问效果一样

直接访问 this super
访问属性 从局部代码块开始查找 从本类开始查找 从直接父类开始查找
访问方法 从局部代码块开始查找 从本类开始查找 从直接父类开始查找
调用构造器 构造器不能被普通方法调用
构造器在对象创建时由系统调用
this(参数列表)
调用本类构造器
必须放在构造器首行
super(参数列表)
调用直接父类构造器
必须放在构造器首行

方法重写/覆盖(override)

方法覆盖(重写)指子类中有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖(重写)了父类的那个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在main中执行 B b = new B();b.say();
// 输出 B

class A{
public void say(){
System.out.println("A");
}
}

class B extends A{
public void say(){
System.out.println("B");
}
}
// B类的say()方法重写了父类A的say()方法
  • 方法重写需要满足下面的条件

    1. 子类方法的参数、方法名要和父类方法的参数、方法名完全一样

      如果参数列表不一样,那么子类方法和父类方法不构成覆盖关系,而是重载关系

    2. 子类方法的返回类型和父类方法的返回类型一样,或者是父类返回类型的子类
      比如父类方法的返回类型是Object,子类方法的返回类型是String

    3. 子类方法不能缩小父类方法的访问权限

      1
      public > protected > 默认 > private

重载与重写

发生范围 方法名 参数列表 返回类型 修饰符
重载(overload) 子类与父类 必须一样 类型、个数、顺序至少有一个不一样 无要求 无要求
重写(override) 子类重写父类 必须一样 相同 要子类方法与父类方法的返回类型一样,或者是父类返回类型的子类 子类方法不能缩小父类方法的访问范围

★多态★

方法或对象具有多种形态,是面向对象的第三大特征,多态(polymorphic)是建立在封装和继承基础之上的

多态的具体体现

  1. 方法的多态

    方法重载体现多态,通过不同的参数调用一个同名方法就可以实现不同的功能。对这个方法来说,这就是多种状态(多态)的体现。

  2. 对象的多态

    • 一个对象的编译类型和运行类型可以不一致

      1
      2
      3
      Animal obj = new Dog();
      //obj的编译类型是Animal,运行类型是Dog
      //可以让父类的引用指向子类的对象
    • 编译类型在定义对象时,就确定了,不能改变

    • 运行类型可以改变

      1
      2
      3
      Animal obj = new Dog();
      obj = new Cat();
      // obj的运行类型变成了Cat,编译类型仍然是Animal
    • 编译类型看定义时等号的左边,运行类型看等号的右边

    • 对象多态示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //Animal.class

    public class Animal {
    public void cry(){
    System.out.println("Animal");
    }
    }

    class Cat extends Animal{
    //子类方法重写父类方法
    public void cry(){
    System.out.println("Cat");
    }
    }

    class Dog extends Animal{
    //子类方法重写父类方法
    public void cry(){
    System.out.println("Dog");
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // Main.class

    public class Main {
    public static void main(String[] args) {
    //编译类型是Animal,运行类型也是Animal
    Animal obj = new Animal();
    //运行类型为Animal,调用Animal类下的cry方法
    obj.cry(); //输出Animal

    obj = new Cat();//运行类型改为Cat
    //调用Cat类下的cry方法
    obj.cry(); //输出Cat

    obj = new Dog();//运行类型改为Dog
    //调用Dog类下的cry
    obj.cry(); //输出Dog
    }
    }

对象多态示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Main.java

public class Main {
public static void main(String[] args) {
Master master = new Master();
Cat cat = new Cat("CAT");
Fish fish = new Fish("FISH");
master.fed(cat,fish); //输出CAT eats FISH

Dog dog = new Dog("DOG");
Bone bone = new Bone("BONE");
master.fed(dog,bone); //输出DOG eats BONE
}
}
1
2
3
4
5
6
7
8
9
//Master.java

public class Master {
//animal的编译类型是Animal,可以指向(接受)Animal的子类对象
//food的编译类型是Food,可以指向(接受)Food的子类对象
public void fed(Animal animal, Food food){
System.out.println(animal.name + " eats " + food.name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Animal.java

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

class Cat extends Animal{
public Cat(String name){
super(name);
}
}

class Dog extends Animal{
public Dog(String name){
super(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Food.java

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

class Bone extends Food{
public Bone(String name){
super(name);
}
}

class Fish extends Food{
public Fish(String name){
super(name);
}
}

多态向上转型和向下转型

  • 多态的前提:两个对象(类)存在继承关系

  • 多态的本质:父类的引用指向了子类的对象

  • 多态的向上转型

    1. 语法,将子类型对象(运行类型)向上赋给为父类型引用(编译类型)

      (父类型的引用指向了子类型的对象)

      1
      父类名 引用名 = new 子类名();
    2. 编译时看编译类型,运行时看运行类型

    3. 可以调用父类(编译类型)中的所有成员(须遵守访问标识符的规则)

    4. 可以调用子类重写父类的成员

    5. 不能调用子类(运行类型)中的特有成员

      因为编译不能通过,在编译阶段对象能调用哪些成员由编译类型来决定

    6. 运行效果看子类(运行类型)的具体实现

      运行时,先在子类中查找方法,子类中有并且可以访问就调用子类,如果子类中没有再在父类中查找

  • 多态的向下转型

    1. 语法,指向子类对象的父类型引用转换为子类型引用

      1
      子类型 引用名 = (子类型)父类引用名;
    2. 只能强转父类的引用,不能强转父类的对象

    3. 要求父类的引用必须指向的是当前目标的对象

    4. 当向下转型后,就可以调用子类类型中所有的成员,包括之前不能被调用的子类的特有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Animal.class

public class Animal {
public void eat(){
System.out.println("Animal eat ...");
}
}

class Cat extends Animal{
public void eat(){
System.out.println("Cat eat ...");
}

public void catch_(){
System.out.println("Cat catch ...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Main.class

public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
animal.eat();
//由于animal的编译类型是Animal,所以它不能访问Cat子类的特殊方法
// animal.catch_();
//向下转型,将父类的引用animal强转成子类的引用cat
//并且animal当前正指向Cat类型的对象
Cat cat = (Cat)animal;
//cat的编译类型是Cat,可以访问Cat的特殊方法
cat.catch_();
}
}

多态的属性

  • 属性没有重写之说!属性的值看编译类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base {
    int num = 0;
    }

    class Sub extends Base {
    int num = 1;
    }

    //在main函数中创建向上转型对象
    Base obj = new Sub();
    //输出obj.num得到编译类型的属性值
    System.out.println(obj.num);//输出0
  • instanceOf比较操作符,用于判断对象的运行类型是否为XX类型或XX类型的子类型

    instance of … 什么的实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Sub sub = new Sub();
    System.out.println(sub instanceof Sub); //输出true
    System.out.println(sub instanceof Base);//输出true

    Base base = new Base();
    System.out.println(base instanceof Sub); //输出false
    System.out.println(base instanceof Base);//输出true

    Base base = new Sub();
    System.out.println(base instanceof Sub); //输出true
    System.out.println(base instanceof Base);//输出true

    //注意必须有关系才能使用instanceof来判断是否为子类型
    //没有父子关系的比较会报错

向上转型和向下转型的练习

  • 练习1
1
2
3
4
5
6
7
8
double d = 3;   //int->double直接转换
int i = (int)d; //double->int需要强转
//---------------------------
Object obj = "Hello"; //引用类型String->Object向上转型
String objStr = (String)obj; //引用类型Object->String向下转型
//---------------------------
Integer objInt = new Integer(3); //引用类型与对象类型对应
Object obj = objInt; //引用类型向上转型
  • 练习2
1
2
3
4
5
6
7
8
9
//main函数中执行
Sub s = new Sub();
System.out.println(s.num); //20
s.showNum(); //20

Base b = s;
System.out.println(b == s); //true
System.out.println(b.num); //10 <--属性看编译类型
b.showNum(); //20 <--方法看运行类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//父类与子类
class Base {
int num = 10;
public void showNum() {
System.out.println(this.num);
}
}

class Sub extends Base {
int num = 20;
public void showNum(){
System.out.println(this.num);
}
}

Java的动态绑定机制

  1. 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Main {
public static void main(String[] args) {
Base base = new Sub();
base.showNum(); //20
//由于base的运行类型是Sub,所有调用方法时先去Sub类中查找
//在Sub类中没有showNum方法,所以去父类Base类中查找
//父类的showNum方法调用getNum方法
//根据动态绑定机制,此getNum方法为Sub类的getNum方法
//getNum返回num,属性没有动态绑定机制,所以直接返回Sub类的20
}
}

class Base {
int num = 10;
public void showNum() {
int num = getNum();
System.out.println(num);
}
public int getNum(){
return num;
}
}

class Sub extends Base {
int num = 20;
// public void showNum(){
// System.out.println(this.num);
// }
public int getNum(){
return num;
}
}

多态数组

多态数组指数组的定义类型为父类类型,但保存的实际元素类型为子类类型

  • 多态数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.poly;

public class Main {
public static void main(String[] args) {
Base[] bases;
bases = new Base[3];
bases[0] = new Base();
bases[1] = new Sub1();
bases[2] = new Sub2();
for(int i = 0; i < 3; i++){
bases[i].say();
System.out.println(bases[i].num);
}
}
}
//输出
//Base
//0
//Sub1
//0
//Sub2
//0

class Base {
int num = 0;
public void say(){
System.out.println("Base");
}
}

class Sub1 extends Base {
int num = 1;
public void say(){
System.out.println("Sub1");
}
}

class Sub2 extends Base {
int num = 2;
public void say(){
System.out.println("Sub2");
}
}
  • 多态数组+调用子类特有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.poly;

public class Main {
public static void main(String[] args) {
Base[] bases;
bases = new Base[3];
bases[0] = new Base();
bases[1] = new Sub1();
bases[2] = new Sub2();
for(int i = 0; i < 3; i++){
//用if条件和instanceof来判断子类类型
//用向下转型来调用子类特有方法
if(bases[i] instanceof Sub1){
((Sub1) bases[i]).subMethod1();
} else if (bases[i] instanceof Sub2) {
((Sub2) bases[i]).subMethod2();
} else if (bases[i] instanceof Base) {
System.out.println("Nothing to do.");
}
}
}
}

class Base {

public void say(){
System.out.println("Base");
}
}

class Sub1 extends Base {

public void subMethod1(){
System.out.println("subMethod1");
}
}

class Sub2 extends Base {

public void subMethod2(){
System.out.println("subMethod2");
}
}

多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.poly;

public class Main {
public static void main(String[] args) {
Base bases[] = new Base[2];
bases[0] = new Sub();
bases[1] = new Base();

for (int i = 0; i < 2; i++) {
//调用方法
show(bases[i]);
}
}
//方法,形参为父类类型,实参可以为子类类型
public static void show(Base obj){
obj.showName();
}
}

class Base {
String name = "Base";
public void showName(){
System.out.println(this.name);
}
}

class Sub extends Base {
String name = "Sub";
public void showName(){
System.out.println(this.name);
}
}

查看Jdk源码

查看Jdk源码

将光标放置需要查看的类或者方法上,使用快捷键Ctrl+B
或者右键需要查看的类或方法Go To > Declaration or Usages

配置jdk source源文件(IDEA默认是已经配置好jdk源码)

file > Project Structure > SDKs > Sourcepath中添加jdk1.8.0_xxx目录下的两个source文件

  • javafx-src.zip
  • src.zip

例如这台电脑的jdk安装目录为D:\Program Files\Java\jdk1.8.0_333,所以添加下面的两个文件
D:\Program Files\Java\jdk1.8.0_333\src.zip
D:\Program Files\Java\jdk1.8.0_333\javafx-src.zip

Object类详解

Object的jdk文档

  • equal()
  • finalize()
  • getClass()
  • hashCode()
  • toString()

equals方法

  • ==

    1. ==:既可以判断基本类型,又可以判断引用类型
    2. ==:如果判断基本类型,判断的是值是否相等
    3. ==:如果判断引用类型,判断的是地址是否相等(即判定是否指向同一个对象)
  • 示例

    1
    2
    3
    4
    String str1 = new String("hello");
    String str2 = new String("hello");
    System.out.println(str1 == str2); //返回false,地址不同
    System.out.println(str1.equals(str2));//返回true,内容相同
  • String类中equals()方法的jdk源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public final class String implements ... {  

...

public boolean equals(Object anObject) {
//比较当前String类型的引用和作为参数被传入的类引用
//它们存储的地址是否相同,即是否指向同一个对象
if (this == anObject) {
return true;
}
//判断anObject是否指向String类型的对象
//如果是,那么一个字符一个字符得比较两个字符串的内容是否相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

...

}
  • Object类中的equals()方法的jdk源码
1
2
3
4
5
6
7
8
public class Object {
...
//直接比较是否指向同一个对象,并不比较对象中的内容是否一样
public boolean equals(Object obj) {
return (this == obj);
}
...
}
  • Integer也重写了Object的equals方法
1
2
3
4
5
6
7
8
9
10
public final class Integer extends Number implements Comparable<Integer> {
...
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
...
}

hashCode方法

返回对象的哈希码值,支持此方法是为了提高哈希表的性能

  • 两个引用,如果指向同一个对象,则哈希值肯定是一样的

  • 两个引用,如果指向的是不同对象,则哈希值是不一样的(有碰撞的可能,但概率极小)

  • 哈希值主要是根据地址来计算的,可以将其和地址对应起来,但不能将哈希值等价于地址

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // main中执行
    A obj1 = new A();
    A obj2 = new A();
    A obj3 = obj2;
    System.out.println("obj1.hashCode() = " + obj1.hashCode());
    System.out.println("obj2.hashCode() = " + obj2.hashCode());
    System.out.println("obj3.hashCode() = " + obj3.hashCode());

    //输出
    obj1.hashCode() = 460141958
    obj2.hashCode() = 1163157884
    obj3.hashCode() = 1163157884
  • 后面在集合中,如果需要的话,往往会重写hashCode方法

toString方法

Object的toString方法(默认)返回:全类名+@+哈希值的十六进制

  • jdk源码
1
2
3
4
5
6
7
8
//Object类的toString方法的jdk源码
public class Object {
...
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
...
}
  • 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.poly;

public class Main {
public static void main(String[] args) {
A obj = new A();
System.out.println(obj.toString());
//输出全类名+@+哈希值十六进制:com.poly.A@1b6d3586
int objHash = obj.hashCode();
System.out.println(Integer.toHexString(objHash));
//输出obj.hashCode()的十六进制:1b6d3586
}
}

class A{

}
  • toString方法往往会重写

    使用快捷键 Alt+Fn+F12选择toString()

  • 当直接输出一个对象时,toString方法会被默认调用

    1
    2
    3
    4
    5
    6
    System.out.println(obj);
    System.out.println(obj.toString());

    //两个语句是等价的,都输出
    com.poly.A@1b6d3586
    com.poly.A@1b6d3586

finalize方法

当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法

  1. 当对象被回收的时候,系统自动调用该对象的finalize方法
  2. 什么时候被回收:当某个对象没有任何引用时,则jvm就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize方法
  3. 垃圾回收机制的调用,由系统来决定,也可以通过System.gc()主动触发垃圾回收机制

断点调试(Debug)

断点调试是指在程序的某一行设置一个断点, 调试时, 程序运行到这一行就会停住, 然后你可以一步一步往下调试, 调试过程中可以看到各个变量当前的值, 出错的话, 调试到出错的代码行即显示错误, 停下. 进行分析从而找到程序的Bug.

断点调试也可以用于帮助程序员观察代码的运行逻辑, 帮助理解代码.

四个基本方法

  1. Continue : 继续执行到程序结束或下一个断点
  2. Step Over : 执行当前行
  3. Step Into : 进入到方法体内
  4. Step Out : 跳出方法体

查看JDK源码

1
2
3
4
5
6
7
8
9
10
import java.util.Arrays;

public class Hello
{
public static void main(String[] args)
{
int[] arr = {1, -1, 10, -20, 100};
Arrays.sort(arr);
}
}

上面的代码中, 将断点设置在 Arrays.sort(arr);进行Debug, 点击 Step Into , 默认情况IDEA并没有进入方法体而是直接执行, 如同 Step Over. 如下两种方法都可以进入方法体, 查看JDK源码

方法一: 直接使用Force Step Into, 快捷键alt+shift+f7

方法二: File>Settings>Build, Execution, Deployment>Debugger>Stepping>Do not step into the classes下的方框中去掉java.*和javax.*的勾选

Note: 进入方法体用 Step Into, 跳出方法体用 Step Out