Java

Java编程知识

1. 基本背景与流程

1.1 JAVA技术体系平台

  • JavaSE(Java Standard Edition标准版)
  • JavaEE(Java Enterprise Edition企业版)
  • JavaME(Java Micro Edition小型版)

Java语言的8大特性:

  1. 简单:比C/C++简单
  2. 面向对象:关注的是有功能的对象。
  3. 分布式:基于网络的多主机协作。
  4. 健壮性:强类型(所有数据都有类型),异常处理,GC(垃圾自动收集),指针(Pointer)的安全化:引用(Reference)。
  5. 安全:所有程序.class必须由ClassLoader类加载器加载。
  6. 跨平台:不同平台有不同的JVM。
  7. 性能好:Java是编译程序,比解释程序好。
  8. 多线程

1.2 JVM、JRE、JDK

  • JVM(Java Virtual Machine ):Java虚拟机,是运行所有Java程序的假想计算机,是Java程序的运行环境之一,也是Java 最具吸引力的特性之一。我们编写的Java代码,都运行在JVM 之上。
  • **JRE ** (Java Runtime Environment) :是Java程序的运行时环境,包含JVM 和运行时所需要的核心类库
  • JDK (Java Development’s Kit):是Java程序开发工具包,包含JRE 和开发人员使用的工具。

想要运行一个已有的Java程序,那么只需安装JRE 即可。

想要开发一个全新的Java程序,那么必须安装JDK ,其内部包含JRE

1561383524152

1.3 环境搭建

1、JDK下载

​ 官方网站www.oracle.com下载JDK8。

2、JDK安装

​ 安装到纯英文路径并复制路径。

3、配置环境变量

​ 环境变量->系统变量->编辑Path->在最前面键入路径;

1.4 Java程序结构

1
2
3
4
5
类{
方法{
语句;
}
}
  • 类>多个方法>多个语句
  • 类中包含方法,方法中也可以有内部类
  • 是java程序的基本单位
  • 方法是一个类最基本的功能单位,表达类或对象的功能,方法必须隶属于类
  • 语句是最小执行单位,必须隶属于方法
  • 主类是包含主方法(main)的类,可以有多个,只有主类可以当作程序运行
  • 公共类是有public关键字的类,公共类要与源文件同名,公共类只能有一个

1.5 运行案例(经典HelloWorld案例)

JAVA开发三步骤:编写、编译、运行。

开发步骤

1、编写Java文件

1
2
3
4
5
public class HelloWorld {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}

2、编译Java源文件生成.class字节码文件

​ 进入Java文件所在目录,在地址中输入cmd即可定位在该目录使用DOS命令行。

1
javac HelloWorld.java

​ 可以发现在该目录生成了HelloWorld.class字节码文件。

3、运行Java程序

1
java HelloWorld

1.6 注意事项

  1. Java语言是一门严格区分大小写的语言

  2. 字符编码问题

下·

1
javac -encoding utf-8 Review01.java
  1. 当类是public时,源文件名必须与类名一致。无论是否public都尽量保持同名。
  2. 一个源文件可以有多个类,编译后生成多个class文件。但是一个源文件只有一个public类。
  3. main方法不是必须在public类中,一般是public。

1.7 注释

​ 单行注释 //

​ 多行注释 /* */

​ 文档注释(java特有)/** */

1.8 DOS指令

1681909436972

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//切换到目标目录-绝对
cd E:\Atguigu\05_code\JavaSE\day01

//切换到目标目录-相对
cd .\Atguigu\05_code

//需要切换盘符情况下的绝对路径,例如当前在C盘需要切换到E盘
cd /d E:\Atguigu\05_code\JavaSE\day01

//回到根目录
cd \

//退回到上一级
cd ..

//多次退回
cd ..\..

//以当前窗口启动资源管理器
start .

1.9 Java字符

1、特殊的转义字符

1
2
3
4
5
6
7
\n:换行
\r:回车
\t:Tab键
\\:\
\":"
\':'
\b:删除键Backspace

2、 Unicode编码值

在JVM内存中,一个字符占2个字节,Java使用Unicode字符集来表示每一个字符,即每一个字符对应一个唯一的Unicode编码值。

范围是0~65535.

字符 Unicode编码值
‘0’ 48
‘1’ 49
‘A’ 65
‘B’ 66
‘a’ 97
1
2
3
4
5
6
7
8
char c2 = 97;
System.out.println(c2);//a

//查看某个字符的Unicode编码
int codeOfA = 'A';
System.out.println(codeOfA);
int codeOfTab = '\t';
System.out.println(codeOfTab);

\u字符的Unicode编码值的十六进制型

1
2
char c = '\u0041'; //十进制Unicode值65,对应十六进制是41,但是\u后面必须写4位
char c = '\u5c1a'; //十进制Unicode值23578,对应十六进制是5c1a

1.10 快捷键

  • Ctrl+D 复制一行
  • Ctrl+Y 删除一行
  • Alt+回车 快速修正常见问题
  • 锁定小键盘后,按1到行尾,按7到行首。
  • Ctrl+P 显示提醒方法的参数列表
  • alt+insert 快速构造 构造器 get/set —–> alt+n 不选
  • ctrl+alt+insert 新建类
  • 快速创建数组 arr.fori
  • 重写方法快捷键:Ctrl+O
  • shift+tab 切换类型
  • 按两次shift , 勾上include non- .. ,可以搜索.
  • 生成构造器 Alt+ Insert
  • 查看构造器和方法形参列表 Ctrl+P

Debug模式:

  • f7 进入语句内部(如果有方法调用,会进入)
  • alt+shift+f7 强行进入最细节
  • f8 一行一行地执行
  • shift+f8 直接结束掉当前方法
  • f9 直接执行

1.11 经典报错

  1. 0 / 0 报错ArithmeticError
  2. NullPointerException 空指针异常,
  3. 0.0 / 0 不报错 显示NaN —>Not a number
  4. 1.0 / 0 不报错 显示Infinity
  5. NumberFormatException 数字格式化异常
  6. ArrayIndexOutOfBoundsException 数组下标越界异常
  7. ClassCastException 类型转换异常
  8. RuntimeException 运行时异常

2. Java基础语法

2.1 关键字(keyword)

关键字:是指在程序中,Java已经定义好的单词,具有特殊含义。全部是小写字母。

保留字:是指在现阶段还不是关键字, 但是将来有可能会成为关键字。

1555209180504

2.2 标识符(identifier)

程序员自己命名的部分可以称为标识符。

即给类、变量、方法、包等命名的字符序列,称为标识符。

1、标识符的命名规则

  1. 标识符只能使用26个英文字母、数字、_和$。
  2. 不能使用关键字。
  3. 数字不能开头。
  4. 不能包含空格,长度无限制。
  5. 严格区分大小写。

2、标识符的命名规范

  1. 包名:全部小写,单词间.分割。

例:java.lang

  1. 类名、接口名:每个单词首字母大写。

例:HelloWorld,String

  1. 变量、方法名:第二个单词开始首字母大写。

例:name,bookName

  1. 常量名:全部大写

例:MAX_VALUE

2.3 数据类型(data type)

Java的数据类型分为两大类:

  • 基本数据类型:包括 整数浮点数字符布尔。 空间中保存数据本身.—>存放在栈中(寿命短)
  • 引用数据类型:包括数组接口枚举注解。空间中保存其他数据的地址. —->存放在堆中(寿命长)
image-20210628142322228

1、基本数据类型存储范围

1681909679895

image-20240223100812343

min max
byte 0x80 0x7F
char 0x0000 0xFFFF
short 0x8000 0x7FFF
int 0x8000 0000 0x7FFF FFFF
long 0x8000 0000 0000 0000 0x7FFF FFFF FFFF FFFF

2、数据类型的作用

1.决定空间大小

2.决定空间中可以保存什么数据

3.决定数据能做什么

2.4 变量(variable)

变量是什么?

在内存中的一块固定类型的空间,此空间可以保存一个数据,且此空间数据在其数据范围内可随意变化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
数据类型  变量名;
例如:
//存储一个整数类型的年龄
int age;

//存储一个小数类型的体重
double weight;

//存储一个单字符类型的性别
char gender;

//存储一个布尔类型的婚姻状态
boolean marry;

//存储一个字符串类型的姓名
String name;

//声明多个同类型的变量
int a,b,c; //表示a,b,c三个变量都是int类型。

注意:变量的数据类型可以是基本数据类型,也可以是引用数据类型。

1、变量赋值

1
2
3
4
5
int age = 18;
double weight = 84.4;
char gender = '男';
boolean marry = true;
String name = "佟刚";
  • long类型:如果赋值的常量整数超过int范围,那么需要在数字后面加L。
  • float类型:如果赋值为常量小数,那么需要在小数后面加F。
  • char类型:使用单引号’’
  • String类型:使用双引号””
1
2
3
4
5
char c = '尚';//使用单引号
String s = '尚';//错误的,哪怕是一个字符,也要使用双引号

char kongChar = '';//错误,单引号中必须有且只能有一个字符
String kongStr = "";//可以,双引号中可以没有其他字符,表示是空字符串

2、变量输出

1
2
3
4
5
//输出变量的值
System.out.println(age);

//输出变量的值
System.out.println("name" + name + ",age = " + age + ",gender = " + gender + ",weight = " + weight + ",marry = " + marry);

3、变量注意事项

  1. 先声明后使用,只有声明好变量才有空间可用。
  2. 必须初始化再使用。
  3. 必须要有数据类型和变量名,数据类型的作用是3个。变量名的作用是定位空间。
  4. 同一作用域内变量不可以重复声明。
  5. 变量不可以超出作用范围使用,由其声明语句所隶属的{}范围决定。
  6. 变量有其数据范围。

4、变量分类

1)按数据类型分:

基本数据类型(primitive):空间中保存数据本身(itself)。

  • 整数 byte short char int long
  • 浮点数 float double
  • 布尔型 boolean 占用1字节

引用数据类型(reference):空间中保存对象地址(address)。地址:某个字节的编号,占用8字节,编号为0的内存地址为null。

  • 类 class
  • 接口 interface
  • 枚举 enum
  • 注解 @interface
  • 数组 []

2)按声明语句位置分:

  • 局部变量(local):声明在方法中的变量,范围小,寿命短。
  • 成员变量(member):声明在类中方法外的变量。此时变量与方法平级,都是类成员。范围大(全局),寿命长。

2.5 常量(literal value)

常量也有内存空间和数据类型,但是空间中的数据不能改变.常量通常有2种,一种是字面量,另一种是final修饰的量.

常见常量:

类型 举例
整数字面量 12,-23, 1567844444557L
浮点字面量 12.34F,12.34
字符字面量 ‘a’,’0’,‘尚’
布尔字面量 true,false
字符串字面量 ”HelloWorld“
  • 整数常量,超过int范围的必须加L
  • 小数常量,不加F就是默认double类型,如果数据类型是float,在初始化时一定要加F。
  • char常量,使用’’
  • String字符串常量,使用””
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConstantDemo {
public static void main(String[] args) {
//输出整数常量值
System.out.println(12);
System.out.println(-23);
System.out.println(2352654566L);

//输出小数常量值
System.out.println(12.34F);
System.out.println(12.34);

//输出字符常量值
System.out.println('a');
System.out.println('0');

//输出布尔常量值
System.out.println(true);
System.out.println(false);

//输出字符串常量值
System.out.println("HelloWorld");
}
}

2.6 最终变量(final)

最终变量习惯上也称为常量,常量名通常所有字母都大写,每一个单词之间使用下划线分割,从命名上和变量名区分开来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FinalVariableDemo {
public static void main(String[] args){
//定义常量
final int FULL_MARK = 100;//满分
// FULL_MARK = 150;//错误,final修饰的变量,是常量,不能重新赋值

//输出常量值
System.out.println("满分:" + FULL_MARK);

//小王的成绩比满分少1分
int wang = FULL_MARK - 1;
//小尚得了满分
int shang = FULL_MARK;
//小刘得了一半分
int liu = FULL_MARK/2;

//输出变量值
System.out.println("小王成绩:" + wang);
System.out.println("小尚成绩:" + shang);
System.out.println("小刘成绩:" + liu);
}

2.7 基本类型转换(Conversion)

转换方式:自动类型转换和强制类型转换

1、自动类型转换

  1. 当把存储范围小的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围大的变量时。
1
2
3
4
5
6
7
int i = 'A';//char自动升级为int,其实就是把字符的编码值赋值给i变量了
double d = 10;//int自动升级为double

byte b = 127; //右边的整数常量值必须在-128~127范围内
//byte bigB = 130;//错误,右边的整数常量值超过byte范围
long num = 1234567; //右边的整数常量值如果在int范围呢,编译和运行都可以通过,这里涉及到数据类型转换
long bigNum = 12345678912L;//右边的整数常量值如果超过int范围,必须加L,否则编译不通过
  1. 小范围与大范围混合运算,按最大类型运算。
1
2
3
4
5
int i = 1;
byte b = 1;
double d = 1.0;

double sum = i + b + d;//混合运算,升级为double
  1. 当byte,short,char数据类型进行算术运算时,按照int类型处理。
1
2
3
4
5
6
7
byte b1 = 1;
byte b2 = 2;
byte b3 = b1 + b2;//编译报错,b1 + b2自动升级为int

char c1 = '0';
char c2 = 'A';
System.out.println(c1 + c2);//113
  1. 在java中,boolean只有true和false的表现形式,没有1或者0,也不能用1或0转换为Boolean。

2、强制类型转换

取值范围大的转换成范围小的类型。

转换格式:

1
数据类型 变量名 = (数据类型)被强转数据值;  //()中的数据类型必须<=变量的数据类型,一般都是=

(1)当把存储范围大的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围小的变量时,需要强制类型转换,提示:有风险,可能会损失精度或溢出

1
2
3
4
5
6
7
int i = (int)3.14;//强制类型转换,损失精度

double d = 1.2;
int num = (int)d;//损失精度

int i = 200;
byte b = (byte)i;//溢出

(2)当某个值想要提升数据类型时,也可以使用强制类型转换

1
2
3
int i = 1;
int j = 2;
double shang = (double)i/j;

提示:这个情况的强制类型转换是没有风险的。

3、基本数据类型与字符串类型的转换

  1. 任何类型与String类型进行”+”运算时,结果一定是String类型。
1
System.out.println("" + 1 + 2);//12
  1. String类型不能进行强制类型转换。
1
2
3
String str = "123";
int num = (int)str;//错误
int num = Integer.parseInt(str);//该方法可以把str转换为int

练习:

1
2
3
4
5
6
7
String str1 = 4;        				//报错
String str2 = 3.5f + ""; //
System.out.println(str2); //输出:3.5
System.out.println(3+4+"Hello!"); //输出:7Hello
System.out.println("Hello!"+3+4); //输出:Hello!34
System.out.println('a'+1+"Hello!"); //输出:98Hello!
System.out.println("Hello"+'a'+1); //输出:Helloa1

2.8 进制

四种类型的进制来表示10。

(1)十进制:正常表示

System.out.println(10); // 10

(2)二进制:0b或0B开头

System.out.println(0B10); // 2

(3)八进制:0开头

System.out.println(010); // 8

(4)十六进制:0x或0X开头

System.out.println(0X10); // 16

2.9 运算符和标点符号

分类 运算符
算术运算符(7个) +、-、*、/、%、++、–
赋值运算符(12个) =、+=、-=、*=、/=、%=、>>=、<<=、>>>=、&=、|=、^=等
关系运算符(6个) >、>=、<、<=、==、!=
逻辑运算符(6个) &、|、^、!、&&、||
条件运算符(2个) (条件表达式)?结果1:结果2
位运算符(7个) <<, >>, >>>, &, |, ^, ~
Lambda运算符(1个) ->(后面学)

1、位运算符

位运算符 符号解释
& 按位与,当两位相同时为1时才返回1
` `
~ 按位非,将操作数的每个位(包括符号位)全部取反
^ 按位异或。当两位相同时返回0,不同时返回1
<< 左移运算符
>> 右移运算符
>>> 无符号右移运算符
  • byte,short,char在计算时按照int类型处理

如何区分&,|,^是逻辑运算符还是位运算符?

如果操作数是boolean类型,就是逻辑运算符,如果操作数是整数,那么就位运算符。

2、算术运算符

算术运算符 符号解释
+ 取自己int a = 5; +a => 5, int b = -5; +b => -5
- 取相反数 int a = 5; -n => -5, int b = -8, -b => 8
+ 加法运算,字符串连接运算,正号
- 减法运算,负号
* 乘法运算
/ 除法运算,整数/整数结果还是整数, 5 / 3 => 1
% 求余运算,余数的符号只看被除数, 13 % 5 => 3
++-- 自增自减运算
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
int a = 10;
a++; // 后加加

a = 10;
++a; // 前加加

int x = 20;
int y = ++x; // 先加后用, x : 21, y : 21

x = 20;
y = x++; // 先用后加, x : 21, y : 20 先把a中的老值放入临时空间t,再把a++=21,然后把临时空间t的值赋值给b。

a = 10;
a = a++;//此时a的打印是10,原理见上。***

10++;//错误,10是常量,++只能针对变量。

//for循环的优化-优化前
for(int i = 0;i < arr.length;i++){

}
//for循环的优化-优化后
for(int i = 0;i != arr.length;++i){//++i不需要临时内存,'!='的效率优于'-'

}
  • M%N,如果N为负数,忽略负号,如果M是负数,结果也是负数。

3、关系运算符/比较运算符

关系运算符 符号解释
< 比较符号左边的数据是否小于右边的数据,如果小于结果是true。
> 比较符号左边的数据是否大于右边的数据,如果大于结果是true。
<= 比较符号左边的数据是否小于或者等于右边的数据,如果大于结果是false。
>= 比较符号左边的数据是否大于或者等于右边的数据,如果小于结果是false。
== 比较符号两边数据是否相等,相等结果是true。
!= 不等于符号 ,如果符号两边的数据不相等,结果是true。
  • 比较大小的运算只能适用于基本数据类型中的数值型之间。
  • ==和!=可以适用于所有数据类型之间。包括对象和布尔。
1
2
3
boolean b1 = 1 <= a < 10;//错误,自左向右依次判断, 1<=a判断完成后,转为true即boolean类型,而boolean型不能参与比较大小运算。
boolean b1 = 1 <= a && a < 10;//正确
boolean b1 = 1 <= a == true;//正确
  • 累操作比常规操作更方便。累操作不会改变类型。
1
2
3
4
5
6
byte b1 = 50;
b1 = b1 + 10;//报错,右边会自动升级成int型。
b1 += 10;//不会报错

int n = 200;
n *= 3.9;

4、逻辑运算符

逻辑运算符,是用来连接两个布尔类型值的运算符(!除外),运算结果也是boolean值true或者false

逻辑运算符 符号解释 符号特点
& 逻辑与,且 falsefalse
` ` 逻辑或
^ 逻辑异或 相同为false,不同为true
! falsetrue,非truefalse
&& 双与,短路与 左边为false,则右边就不看
` `

&&和&区别,||和|区别:

  • **&&&**区别:

    • &&&结果一样,&&有短路效果,左边为false,右边不执行;&左边无论是什么,右边都会执行。
  • **|||**区别:

    • |||结果一样,||有短路效果,左边为true,右边不执行;|左边无论是什么,右边都会执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class LogicOperator{
    public static void main(String[] args){
    /*
    表示条件,成绩必须在[0,100]之间
    成绩是int类型变量score
    */
    int score = 56;
    System.out.println(0<=score && score<=100);
    /*
    错误:
    System.out.println(0<=score<=100);
    */

5、条件运算符

格式:

1
条件表达式?表达式1:表达式2
  • 表达式1和表达式2必须是一样的数据类型
1
2
3
4
5
6
//找到两者的最大值
public static void main(String[] args){
int n1 = ?;
int n2 = ?;
int max = n1 > n2 ? n1 : n2;
}

6、赋值运算符

运算符 符号解释
= 将右边的常量值/变量值/表达式的值,赋值给左边的变量,. 左面必须是变量
+= 相加,最后将结果赋值给左边的变量
-= 相减,最后将结果赋值给左边的变量
*= 相乘,最后将结果赋值给左边的变量
/= 相除,最后将结果赋值给左边的变量
%= 相模,最后将结果赋值给左边的变量
<<= 将左边变量的值左移右边常量/变量值/表达式的值的相应位,最后将结果赋值给左边的变量
>>= 将左边变量的值右移右边常量/变量值/表达式的值的相应位,最后将结果赋值给左边的变量
>>>= 将左边变量的值无符号右移右边常量/变量值/表达式的值的相应位,最后将结果赋值给左边的变量
&= 将左边变量的值和右边的常量值/变量值/表达式的值进行按位与,最后将结果赋值给左边的变量
|= 将左边变量的值和右边的常量值/变量值/表达式的值进行按位或,最后将结果赋值给左边的变量
^= 将左边变量的值和右边的常量值/变量值/表达式的值进行按位异或,最后将结果赋值给左边的变量
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args){
int a;
a = 10;
System.out.println(a);//打印的是a中的副本
System.out.println(a = 10);//打印表达式本身
//赋值表达式的结果值就是它的最右值。

int n1,n2,n3,n4;
n1 = n2 = n3 = n4 = 100;//从右向左赋值,n3被赋予的是'n4 = 100'的表达式本身的值。
}

7、运算符优先级

1681909834136

口诀:

单目运算排第一;

乘除余二加减三;

移位四,关系五;

等和不等排第六;

位与、异或和位或;

短路与和短路或;

依次从七到十一;

条件排在第十二;

赋值一定是最后;

8、标点符号

image-20210701170438577

  • 小括号()用于强制类型转换、表示优先运算表达式、方法参数列表
  • 大括号{}用于数组元素列表、类体、方法体、复合语句代码块边界符
  • 中括号[]用于数组
  • 分号;用于结束语句
  • 逗号,用于多个赋值表达式的分隔符和方法参数列表分隔符
  • 英文句号.用于成员访问和包目录结构分隔符
  • 英文省略号…用于可变参数
  • @用于注解
  • 双冒号::用于方法引用

3. 流程控制语句

3.1 顺序结构

顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。

3.2 分支语句

1、条件判断:if

1
2
3
if (条件表达式){
语句体;

2、条件判断:if..else

1
2
3
4
5
if(关系表达式) { 
语句体1;
}else {
语句体2;
}

练习:接收一个整数,判断能否被7整除。

1
2
3
4
5
6
7
8
9
10
11
12
public class TestIfElse {
public static void main(String[] args){
int inputNum = Integer.parseInt(args[0]);

if(inputNum % 7 == 0) {
System.out.println(inputNum + "可以被7整除");
} else{
System.out.println(inputNum + "不可以被7整除");
}

}
}

3、多分支条件判断

1
2
3
4
5
6
7
8
9
10
11
if (判断条件1) {
执行语句1;
} else if (判断条件2) {
执行语句2;
}
...
}else if (判断条件n) {
执行语句n;
} else {
执行语句n+1;
}

4、if..else嵌套

1
2
3
4
5
6
7
8
9
if(){
if(){

}else{

}
}else{

}

5、switch..case多分支选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//如果case中没有break,会形成fall through.穿透
//如果某个case一旦进入,后面的所有case形同虚设,直到遇到break中switch结束。
switch(表达式){//必须是非long整数的变量/字符串/枚举
case 常量值1: // if (表达式==常量值1),必须是常量,因为它的底层是一个表格。
语句块1;
break;】
case 常量值2:
语句块2;
break;】
。。。
default: // else
语句块n+1;
break;】

}
  • swtich表达式值的类型只能是4种基本数据类型(byte,short,int,char)、2种引用数据类型(JDK1.5之后的枚举,JDK1.7之后的String)
  • case后面必须是常量且不能重复,多个case绝不允许出现相同的常量。

switch语句具有穿透性。一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到break或者整个switch语句结束,switch语句执行终止。

switch case的效率比for循环高,因为它的底层是表格,里面存储常量,不需要判断,直接跳转。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.util.Scanner;

/*
* 需求:指定一个月份,输出该月份对应的季节。
* 一年有四季
* 3,4,5 春季
* 6,7,8 夏季
* 9,10,11 秋季
* 12,1,2 冬季
*/
public class Test14SwitchDemo2 {
public static void main(String[] args) {
int month = Integer.parseInt(args[0])

/*
switch(month) {
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;
case 8:
System.out.println("夏季");
break;
case 9:
System.out.println("秋季");
break;
case 10:
System.out.println("秋季");
break;
case 11:
System.out.println("秋季");
break;
case 12:
System.out.println("冬季");
break;
default:
System.out.println("你输入的月份有误");
break;
}
*/

// 改进版
switch(month) {
case 1:
case 2:
case 12:
System.out.println("冬季");
break;
case 3:
case 4:
case 5:
System.out.println("春季");
break;
case 6:
case 7:
case 8:
System.out.println("夏季");
break;
case 9:
case 10:
case 11:
System.out.println("秋季");
break;
default:
System.out.println("你输入的月份有误");
break;
}
}
}

3.3 循环语句

for适用于循环次数确定的循环。

while和do while适用于次数不确定。

组成部分

      1. 初始化语句(init_statement)
      2. 循环条件语句(test_statement)
      3. 循环体(body)
      4. 迭代语句(alter_statement)

1、while循环

1
2
3
4
5
[初始化语句;]
while (循环条件语句) {
循环体语句;
[迭代语句;]
}

while中循环条件必须是boolean类。

1
2
3
4
while(1 == 1) //报错,不可能到达。死循环

boolean flag = true;
while(flag) //不会报错,因为flag是变量,java认为有可能退出循环。称为无限循环。无限循环的存在是好的,相当于程序可以一直运行。

2、do…while循环

1
2
3
4
5
[初始化语句;]
do {
循环体语句;
[迭代语句;]
} while (循环条件语句);

注意:

  • while(循环条件)中循环条件必须是boolean类型

  • do{}while();最后有一个分号

  • do…while结构的循环体语句是至少会执行一次,这个和for和while是不一样的

3、for循环

1
2
3
for (初始化语句A; 循环条件语句B; 迭代语句C) { /
循环体语句D;
}

注意:

  • 循环条件必须是boolean类型

死循环:

1
2
3
4
5
6
7
8
9
10
11
while (true) {
语句块;
}

do {
语句块;
} while (true);

for(;;){
循环体语句块;//如果循环体中没有跳出循环体的语句,那么就是死循环
}
增强型for循环
1
2
3
4
5
6
7
8
9
//增强for循环,利用tmp让修改内容的位置在栈中而不是直接修改堆中的内容,更安全适用于只读访问.
for(int tmp : arr){
System.out.println(tmp + " ");
}

//经典for循环,适用于写入操作或和下标密切相关的操作
for(int i = 0;i< arr.length;++i){

}

4、循环语句的区别

  • 从循环次数角度分析
    • do…while循环至少执行一次循环体语句
    • for和while循环先循环条件语句是否成立,然后决定是否执行循环体,至少执行零次循环体语句

5、循环嵌套

1
2
3
4
5
6
for(初始化语句A; 循环条件语句B; 迭代语句C) {//外循环控制行
for(初始化语句D; 循环条件语句E; 迭代语句F) {//内循环控制列
循环体语句G;
}
循环体语句H;
}

6、特殊流程控制break

使用场景:终止switch/当前循环/指定标签

1
2
3
4
for (int i = 0; i < 10; i++) {
if (i == 3) {
break;
}

使用标签中断指定循环

1
2
3
4
5
6
7
8
l1: for(int i = 0;i<5;i++){
l2: for(int j = 0;j<3;j++){
System.out.println("j:" + j)
if(j == 1){
break l1;//中断标签指示的循环
}
}
}

命名的标签可以中断

1
2
3
4
5
6
7
8
boolean flag = true;
l3: {
System.out.println("这是l3的语句");
if(flag == true){//如果不在这里添加flag == true的判断而直接写break l3 后面的输出语句会报错。
break l3;
}
System.out.println("这是break后的语句,如果显示那么就是出错了");
}

7、特殊流程控制continue

使用场景:提前结束本次循环,继续下一次的循环

1
2
3
4
5
6
for (int i = 0; i < 100; i++){
if (i % 10 == 0) {
continue;
}
System.out.println("i : " + i);
}

8、特殊流程控制return

return 并不是用于循环的, 它在方法中表示方法的结束, 而不管此时语句正在循环中还是哪里, 都会直接结束.

9、死循环与无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while(true){};//死循环
while(true);//死循环

boolean flag = true;
while(flag);//无限循环,flag是变量

do{} while(true);//死循环
do{} while(flag);//无限循环

for(;;);//死循环
for(int i = 0; ; i++);//死循环
for(int i = 0;true;i++);//死循环

for(int i = 0;flag;i++);//无限循环
for(int i = 0;i<10;);//无限循环
for(int i = 0;i<10;i--);//有限循环

3.4 方法(Method)

方法也叫函数(function),是java程序中的独立的功能单位,方法是一个类中最基本的功能单元。

1、方法的特点

(1)必须先声明后使用

类,变量,方法等都要先声明后使用

(2)不调用不执行,调用一次执行一次。

2、方法的声明

声明方法的位置==必须在类中方法外==,方法不可以嵌套。

声明方法的位置:

1
2
3
4
5
6
7
8
类{
方法1(){

}
方法2(){

}
}

声明方法的格式:

1
2
3
4
5
【修饰符】 返回值类型 方法名(数据类型1 形参1, 数据类型2 形参2 ....){
语句1;
语句2;
return 返回值;
}

一个完整的方法 = 方法头 + 方法体。

方法头就是 【修饰符】 返回值类型 方法名(【形参列表 】),也称为方法签名,通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。

方法头可能包含5个部分,但是有些部分是可能缺省的:

  • 修饰符:可选的。方法的修饰符也有很多,例如:public、protected、private、static、abstract、native、final、synchronized等。其中根据是否有static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法.
  • 返回值类型: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者
    • 基本数据类型
    • 引用数据类型
    • 无返回值类型:void
  • 方法名:标识符,给方法起一个名字,见名知意,能准确代表该方法功能的名字
  • 参数列表:表示完成方法体功能时需要外部提供的数据列表
  • throws异常列表

关于方法体中return语句的说明:

  • return语句的作用是结束方法的执行,并将方法的结果返回去
  • 如果返回值类型不是void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
  • 如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return ; 就可以。
  • return语句后面就不能再写其他代码了,否则会报错:Unreachable code

3、形参和实参

形参:在定义方法时方法名后面括号中声明的变量称为形式参数

实参:在调用方法时方法名后面括号中的使用的值/变量/表达式称为实际参数

4.值传递

1、对于基本类型参数,在方法体内对参数进行重新赋值,并不会改变原有变量的值。

2、对于引用类型参数,在方法体内对参数进行重新赋予引用,并不会改变原有变量所持有的引用。

3、方法体内对参数进行运算,不影响原有变量的值。

4、方法体内对参数所指向对象的属性进行操作,将改变原有变量所指向对象的属性值。

3.5 方法的重载

  • 方法重载(overload):指在同一个类中,允许存在一个以上的同名方法只要它们的参数列表不同即可,与修饰符和返回值类型无关。

  • 参数列表:数据类型个数不同,数据类型不同(按理来说数据类型顺序不同也可以,但是很少见,也不推荐,逻辑上容易有歧义)。

  • 重载方法调用:JVM通过方法的参数列表,调用匹配的方法。

    • 先找个数、类型最匹配的
  • 再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错

  • 连环调用,对方法重载时调用重载前方法,便于维护。能连环调用绝不单独调用。

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
class OverLoad_2 {

//打印一个n*m的矩形
public static void method1(int n, int m) {
System.out.println("打印" + n + "*" + m + "的矩形");
System.out.println("----------------");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
System.out.print("*");
}
System.out.println();
}
System.out.println("----------------");
}

//打印一个边长为n的正方形并返回周长
public static int method1(int n) {
method1(n, n);//连环调用,能连环调用绝不单独写,便于维护。
return n * 4;
}


public static void main(String[] args) {
int n = Integer.parseInt(args[0]);
int m = Integer.parseInt(args[1]);

method1(n, m);
method1(n);
}
}

3.6 类限定

限定使用某个类的方法。类名.方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JavaBeanTest {
public static void main(String[] args) {
int n = 100;
int m = 20;

System.out.println(Another.add(n,m));//使用Another类中的add方法。
}
}

class Another {
public static int add(int a, int b) {
return a + b;
}
}

3.7 方法的递归调用

递归调用:方法自己调用自己的现象就称为递归。

递归的分类:

  • 递归分为两种,直接递归和间接递归。
  • 直接递归称为方法自身调用自己。
  • 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。

注意事项

  • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
  • 在递归中虽然有限定条件,但是递归深度不能太深,否则效率低下,或者也会发生栈内存溢出。
    • 能够使用循环代替的,尽量使用循环代替递归
  • 递归一定有参数,参数控制就类似于迭代语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FibonacciTest {

public static void main(String[] args) {
System.out.println(f(10));//6765
}

//使用递归
public static int f(int n){//计算斐波那契数列第n个值是多少
if(n<1){//负数是返回特殊值1,表示不计算负数情况
return 1;
}
if(n==1 || n==2){
return 1;
}
return f(n-2) + f(n-1);
}
}

4. 数组Array

4.1 数组的特点

1、数组的长度一旦确定就不能修改

2、创建数组时会在内存中开辟一整块连续的空间。

3、存取元素的速度快,因为可以通过[下标],直接定位到任意一个元素。

4.2 数组的分类

按照维度分:

  • 一维数组
  • 二维数组

按照元素类型分:

  • 基本数据类型的元素:存储数据值
  • 引用数据类型的元素:存储对象(本质上存储对象的首地址)

无论数组的元素是基本数据类型还是引用数据类型,数组本身都是引用数据类型

4.3 数组的声明

1
2
3
4
5
//推荐
元素的数据类型[] 数组的名称;

//不推荐
元素的数据类型 二维数组名[];
  • 数组的声明,就是要确定:

(1)数组的维度:在Java中数组的标点符号是[],[]表示一维,[][]表示二维

(2)数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的Java的数据类型。例如:int, String, Student等

(3)数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据。

4.4 一维数组

1、一维数组的静态初始化

数组初始化

初始化就是确定数组元素的个数和元素的值

静态数组初始化

静态初始化就是用静态数据为数组初始化,此时数组的长度由静态数据的个数决定。

静态初始化格式1:

1
数据类型[] 数组名 = {元素1,元素2,元素3...};//必须在一个语句中,不能分开两个语句写
1
2
3
4
int[] arr = {1,2,3,4,5};//正确

int[] arr;
arr = {1,2,3,4,5};//错误

静态初始化格式2:

1
2
3
4
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};

数据类型[] 数组名;
数组名 = new 数据类型[]{元素1,元素2,元素3...};
1
2
3
4
int[] arr = new int[]{1,2,3,4,5};//正确

int[] arr;
arr = new int[]{1,2,3,4,5};//正确

2、一维数组的使用

  1. 数组长度
1
数组名.length
  1. 数组索引
1
数组名[下标];//从0开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test03ArrayUse {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};

System.out.println("arr数组的长度:" + arr.length);
System.out.println("arr数组的第1个元素:" + arr[0]);//下标从0开始
System.out.println("arr数组的第2个元素:" + arr[1]);
System.out.println("arr数组的第3个元素:" + arr[2]);
System.out.println("arr数组的第4个元素:" + arr[3]);
System.out.println("arr数组的第5个元素:" + arr[4]);

//修改第1个元素的值
//此处arr[0]相当于一个int类型的变量
arr[0] = 100;
System.out.println("arr数组的第1个元素:" + arr[0]);
}
}

3、越界异常

1
2
3
4
5
6
7
8
public class Test04ArrayIndexOutOfBoundsException {
public static void main(String[] args) {
int[] arr = {1,2,3};
// System.out.println("最后一个元素:" + arr[3]);//错误,下标越界ArrayIndexOutOfBoundsException
// System.out.println("最后一个元素:" + arr[arr.length]);//错误,下标越界ArrayIndexOutOfBoundsException
System.out.println("最后一个元素:" + arr[arr.length-1]);//对
}
}

4、一维数组的遍历

1
2
3
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}

5、一堆数组动态初始化

先确定元素的个数(即数组的长度),而元素此时只是默认值,并不是真正的数据。元素真正的数据需要后续单独一个一个赋值。

1
2
3
4
5
6
//第一种
数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度];

//第二种
数组存储的元素的数据类型[] 数组名字;
数组名字 = new 数组存储的元素的数据类型[长度];
  • new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用new创建数组对象。
  • 注意:数组有定长特性,长度一旦指定,不可更改。
1
2
3
4
5
6
int[] arr = new int[5];

int[] arr;//在栈中开辟一个引用变量的空间,用于保存一个数组对象,此时并没有地址。
arr = new int[5];//new操作会在堆内存中创建一个真实的数组对象。占用20字节,并且所有数据都是0。

int[] arr = new int[5]{1,2,3,4,5};//错误的,后面有{}指定元素列表,就不需要在[]中指定元素个数了。
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
public class ArrayInitTest {
public static void main(String[] args) {
int[] arr = new int[5];

System.out.println("arr数组的长度:" + arr.length);
System.out.print("存储数据到arr数组之前:[");
for (int i = 0; i < arr.length; i++) {
if(i==0){
System.out.print(arr[i]);
}else{
System.out.print("," + arr[i]);
}
}
System.out.println("]");

//初始化
/* arr[0] = 2;
arr[1] = 4;
arr[2] = 6;
arr[3] = 8;
arr[4] = 10;*/

for (int i = 0; i < arr.length; i++) {
arr[i] = (i+1) * 2;
}

System.out.print("存储数据到arr数组之后:[");
for (int i = 0; i < arr.length; i++) {
if(i==0){
System.out.print(arr[i]);
}else{
System.out.print("," + arr[i]);
}
}
System.out.println("]");
}
}

6、数组元素的默认值

当我们使用动态初始化方式创建数组时,元素只是默认值。

7、一维数组内存分析

Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。

Java虚拟机的内存划分:

1561465258546

区域名称 作用
程序计数器 程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址
本地方法栈 当程序中调用了native的本地方法时,本地方法执行期间的内存区域
方法区 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆内存 存储对象(包括数组对象),new来创建的,都存储在堆内存。
虚拟机栈 用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。

1. 一维数组在内存中的存储

1
2
3
4
public static void main(String[] args){
int[] arr = new int[3];
System.out.println(arr);//[I@5f150435
}

程序执行流程:

1.main方法进入方法栈执行。

2.创建数组,JVM会在堆内存中开辟空间,存储数组。

3.数组在内存中会有自己的内存地址,以十六进制表示。

4.数组中有3个元素,默认值0。

5.JVM将数组的内存首地址赋值给引用类型变量arr。

6.变量arr中保存的是数组内存中的地址,而不是一个具体是数值,因此是引用数据类型。

思考:打印arr为什么是[I@5f150435,它是数组的地址吗?

答:它不是数组的地址。

问?不是说arr中存储的是数组对象的首地址吗?

答:arr中存储的是数组的首地址,但是因为数组是引用数据类型,打印arr时,会自动调用arr数组对象的toString()方法,该方法默认实现的是对象类型名@该对象的hashCode()值的十六进制值。

问?对象的hashCode值是否就是对象内存地址?

答:不一定,因为这个和不同品牌的JVM产品的具体实现有关。例如:Oracle的OpenJDK中给出了5种实现,其中有一种是直接返回对象的内存地址,但是OpenJDK默认没有选择这种方式。

两个变量指向同一个数组,本质上代表同一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 定义数组,存储3个元素
int[] arr = new int[3];
//数组索引进行赋值
arr[0] = 5;
arr[1] = 6;
arr[2] = 7;
//输出3个索引上的元素值
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
//定义数组变量arr2,将arr的地址赋值给arr2
int[] arr2 = arr;
arr2[1] = 9;
System.out.println(arr[1]);
}

4.5 数组的常见算法

1、数组统计

​ 求总和、均值、统计偶数等。

2、数组找最值

1、找最值并记录下标

思路1:假设第一个最大/最小,然后用max/min与后面元素依次比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ArrayFindMaxTest{
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
int max = array[0];
int maxIndex = 0;
for(int i = 1;i<array.length;i++){
if(array[i] > max){
max = array[i];
index = i;
}
}
System.out.println("最大值为:" + max);
System.out.println("最大值的下标是:" + maxIndex);
}
}

思路2:用maxIndex时刻记录目前比对的最大/小的下标,那么arr[maxIndex]就是目前的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
class ArrayFindMaxTest2{
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
int maxIndex = 0;
for(int i = 1;i< array.length;i++){
if(array[i] > array[maxIndex]){
maxIndex = i;
}
}
System.out.println("最大值为:" + array[maxIndex]);
System.out.println("最大值的下标是:" + maxIndex);
}
}

3、数组查找

1、顺序查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Scanner;

public class ArrayOrderSearch {
public static void main(String[] args) {
//找到查找到的第一个数字的位置.
int[] array = {1, 2, 3, 4, 5, 5, 6, 7, 5};

//输入要查找的数字
Scanner input = new Scanner(System.in);
System.out.println("请输入您要查找的数字:");
int searchNum = input.nextInt();

//查找
for (int i = 0; i < array.length; ++i) {
if (array[i] == searchNum) {
System.out.println("数字的下标是: " + i);
break;
}
}
}
}

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
33
34
35
36
37
38
39
40
41
42
43
import java.util.Scanner;

public class BinarySearch {
public static void main(String[] args) {
/*
二分查找的前提是数组是整体有序的。数组默认是递增的。
首先选择数组中间的数字和目标数字对比。
如果相等则直接返回答案。
如果不相等-中间的数字大于目标值,则中间向右的均大于目标值,全部排除,right = mid - 1。
-中间的数字小于目标值,则中间向左的均小于目标值,全部排除,left = mid + 1。
进行下一次对比。
*/

//数组要有序,默认是递增。
int[] array = {8, 19, 21, 34, 39, 40};

//输入要查找的整数。
Scanner input = new Scanner(System.in);
System.out.println("请输入您要查找的值: ");
int target = input.nextInt();

//二分查找
int index = -1;//默认-1,找不到就输出-1.
for (int left = 0, right = array.length - 1; left <= right; ) {
int mid = left + (right - left) / 2;

if (array[mid] == target) {
index = mid;
break;
} else if (target > array[mid]) {
left = mid + 1;//目标大于中间值,说明目标左侧值都比目标小,left移动到mid+1位置。
} else if (target < array[mid]) {
right = mid - 1;//目标小于中间值,说明目标右侧值都比目标大,right移动到mid-1位置。
}
}

if(index!=-1){
System.out.println("找到了,目标位置是" + index);
}else {
System.out.println("没有找到。");
}
}
}

考虑重复元素

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import java.util.Scanner;

public class BinarySearch {
public static void main(String[] args) {
/*
二分查找的前提是数组是整体有序的。数组默认是递增的。
首先选择数组中间的数字和目标数字对比。
如果相等则直接返回答案。
如果不相等-中间的数字大于目标值,则中间向右的均大于目标值,全部排除,right = mid - 1。
-中间的数字小于目标值,则中间向左的均小于目标值,全部排除,left = mid + 1。
进行下一次对比。
*/

//数组要有序,默认是递增。
int[] array = {8, 19, 21, 34, 39, 40};

//输入要查找的整数。
Scanner input = new Scanner(System.in);
System.out.println("请输入您要查找的值: ");
int target = input.nextInt();

//二分查找
int index = -1;//默认-1,找不到就输出-1.
for (int left = 0, right = array.length - 1; left <= right; ) {
int mid = left + (right - left) / 2;

if (array[mid] == target) {
index = mid;
break;
} else if (target > array[mid]) {
left = mid + 1;//目标大于中间值,说明目标左侧值都比目标小,left移动到mid+1位置。
} else if (target < array[mid]) {
right = mid - 1;//目标小于中间值,说明目标右侧值都比目标大,right移动到mid-1位置。
}
}

if (index != -1) {
System.out.println("找到了,目标位置是" + index);
} else {
System.out.println("没有找到。");
}
}
}

class BinarySearch_2 {
public static void main(String[] args) {
/*
如果数组中有重复元素,需要考虑重复元素时,二分查找应该怎么做。
*/

//数组必须有序。
int[] array = {1, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10};

//输入需要查找的数
Scanner input = new Scanner(System.in);
System.out.println("请输入要查找的数:");
int searchNum = input.nextInt();

//二分查找
int index = -1;//如果查找不到,返回-1.
for (int left = 0, right = array.length - 1; left <= right; ) {
int mid = left + (right - left) / 2;
System.out.println("mid = " + mid);

if (array[mid] == searchNum) {
index = mid;
System.out.print(searchNum + "的下标有:" + index);

//使用二分查找的数是"有序",如果有重复元素,那么他们是在一起的。
//继续比较mid左边和右边的元素,直到元素与target不相等为止。

for (int i = mid - 1; i >= left; i--) {
if (array[i] == searchNum) {
System.out.print("," + i);
} else {
break;
}
}
for (int j = mid + 1; j <= right; j++) {
if (array[j] == searchNum) {
System.out.print("," + j);
} else {
break;
}
}
System.out.println();
break;
} else if (searchNum > array[mid]) {
//说明target在mid右边
left = mid + 1;

//如果array右边的元素和array[mid]一样,可以把左边界向右侧移动。
while (array[mid] == array[left]) {
System.out.println("left = " + left);
left++;
}
} else {
//说明target在mid左边
right = mid - 1;

//如果array左边的元素和array[mid]一样,可以把右边界向左侧移动。
while (array[mid] == array[right]) {
System.out.println("right = " + right);
right--;
}
}
}
if (index == -1) {
System.out.println("未找到目标元素。");
}
}
}

4、数组反转

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
//数组对称位置的元素互换
public class ArrayReverse {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};//设置数组

//循环遍历打印反转前数组
System.out.print("反转前:");
System.out.print(array[0]);
for (int i = 1; i < array.length; i++) {
System.out.print("," + array[i]);
}
System.out.println();

//反转数组
for (int i = 0; i < array.length / 2; i++) {
int temp = array[i];
array[i] = array[array.length - 1 - i];
array[array.length - 1 - i] = temp;
}

//循环遍历打印反转后数组
System.out.print("反转后:");
System.out.print(array[0]);
for (int i = 1; i < array.length; i++) {
System.out.print("," + array[i]);
}


}
}

5、数组排序

1、排序算法

image-20211222111142684

2、直接选择排序

又称为简单选择排序,从待排序序列中选出最小/最大元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找最小/最大元素,放到已排序序列的末尾。直到全部待排序的数据元素的个数为0。选择排序不稳定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//直接选择排序
//思想:每一轮找出本轮的最大值/最小值,然后看他是否在正确的位置,如果不在就与正确的位置交换。
public static void selectSort(int[] arr) {
//每次找到最小值放入到i的位置,然后i++,在下一个位置放入最小值.
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;//记录最小值的下标
for (int j = i; j < arr.length; j++) {
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
int tmp = arr[i];//交换最小值和i位置的元素
arr[i] = arr[minIndex];
arr[minIndex] = tmp;
}
}
3、冒泡排序

原理:比较两个相邻的元素,将值大的元素交换至右端。

思路:依次比较相邻的两个数,将小数放到前面,大数放到后面。

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
//冒泡排序 BubbleSort

public class BubbleSort {
public static void main(String[] args) {
int[] array = {1, 4, 2, 3, 6, 7, 8, 9, 4, 5, 6};

//从左到右依次比较,将最大值移动到待比较队列最右。
for (int i = array.length - 1; i > 0; i--) {//用i控制待排序队列的最右侧位置
for (int j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {//相邻之间如果左侧大于右侧,则把左侧值交换到右侧。
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}

//输出
System.out.print(array[0]);
for (int i = 1; i < array.length; i++) {
System.out.print("," + array[i]);
}
System.out.println();
}
}
4、冒泡排序优化
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
//多加一个flag来控制比较次数,如果某一次排序过程中,整个数组顺序都无变动,说明已经是有序状态。

//冒泡排序 BubbleSort

public class BubbleSort {
public static void main(String[] args) {
int[] array = {1, 4, 2, 3, 6, 7, 8, 9, 4, 5, 6};

//从左到右依次比较,将最大值移动到待比较队列最右。
for (int i = array.length - 1; i > 0; i--) {//用i控制待排序队列的最右侧位置
for (int j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {//相邻之间如果左侧大于右侧,则把左侧值交换到右侧。
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}

//输出
System.out.print(array[0]);
for (int i = 1; i < array.length; i++) {
System.out.print("," + array[i]);
}
System.out.println();
}
}

class BubbleSort2 {
public static void main(String[] args) {
int[] array = {1, 4, 2, 3, 6, 7, 8, 9, 4, 5, 6};

for (int i = array.length - 1; i > 0; i--) {
boolean flag = true;
for (int j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
flag = false;
}
}

if(flag){
break;
}
}

//输出排序后数组
System.out.print(array[0]);
for (int i = 1; i < array.length; i++) {
System.out.print("," + array[i]);
}
System.out.println();
}
}
5、快速排序

分区\递归

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 static void QuickSort(int left, int right, int... arr) {

//判定递归结束条件,right == left
if (right <= left) {
return;
}

//需要定义pivot,first,last.为了方便起见,pivot直接就定义成first位置的元素
int first = left;//数组最左侧元素下标
int last = right;//数组最右侧元素下标
int pivot = arr[left];//pivot默认为下标为left的元素,在一趟快速排序后,应当实现的效果是:pivot左侧都比pivot位置元素小,pivot右侧都比pivot位置元素大.

//left < right的情况下就一直进行循环,直到left == right
while (first < last) {
//从right指针开始,将right位置与基准值进行比较
while (first < last && pivot <= arr[last]) {//如果left < right 且 right位置的元素比基准值大 那就right--,继续向左探寻
last--;
}
arr[first] = arr[last];//如果left < right 且 right位置的元素比基准值小 那就把right指针的值放入left指针位置

//right探测完毕后,将left位置与基准值进行比较
while (first < last && pivot >= arr[first]) {//如果left < right 且 left位置的元素比基准值小,那就left++,继续向右探寻
first++;
}
arr[last] = arr[first];//如果left < right 且 left位置的元素比基准值大,那就把left指针的元素放入到right指针元素.
}

arr[first] = pivot;

//分别递归左右子数列,直到数列中只剩下一个元素
QuickSort(left, last - 1, arr);
QuickSort(first + 1, right, arr);
}
6、idea中的排序调用
1
arrays.

6、对象数组

数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用数据类型是,我们称为对象数组。

注意:对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。

1、对象数组的声明和使用

案例:

(1)定义矩形类,包含长、宽属性,area()求面积方法,perimeter()求周长方法,String getInfo()返回圆对象的详细信息的方法

(2)在测试类中创建长度为5的Rectangle[]数组,用来装3个矩形对象,并给3个矩形对象的长分别赋值为10,20,30,宽分别赋值为5,15,25,遍历输出

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
class Rectangle {
double height;
double width;

double area() {//面积
return height * width;
}

double perimeter() {//周长
return 2 * (height + width);
}

String getInfo() {
return "长:" + height +
",宽:" + width +
",面积:" + area() +
",周长:" + perimeter();
}
}

public class ObjectArrayTest {
public static void main(String[] args) {
//声明并创建一个长度为3的矩形对象数组
Rectangle[] rect = new Rectangle[3];

//创建3个矩形对象,并为3个对象的实例变量赋值
//3个矩形对象的长分别是10,20,30
//3个矩形对象的宽分别是5,15,25
//调用矩形对象的getInfo()返回对象信息后输出
for (int i = 0; i < rect.length; i++) {
//创建矩形对象
rect[i] = new Rectangle();

//为矩形对象的成员变量赋值
rect[i].height = (i + 1) * 10;
rect[i].width = (2 * i + 1) * 5;

//获取并输出对象的信息
System.out.println(rect[i].getInfo());
}
}
}

4.6 二维数组

1. 二维数组的概念

如果需要同时存储多组同类型的数据,就需要使用二维数组。

二维数组:本质上就是元素为一维数组的一个数组。

二维数组的标记:[][]

1
int[][] arr; //arr是一个二维数组,可以看成元素是int[]一维数组类型的一维数组

二维数组声明的语法格式:

1
2
3
4
5
6
7
//推荐
元素的数据类型[][] 二维数组名;

//不推荐
元素的数据类型 二维数组名[][];
//不推荐
元素的数据类型[] 二维数组名[];

例如:

1
2
3
4
5
6
7
8
9
public class ArrayDefine {
public static void main(String[] args) {
//存储多组成绩
int[][] grades;

//存储多组姓名
String[][] names;
}
}

面试:

1
int[] x,y[];//x是一维数组,y是二维数组f

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
//以下格式要求声明与静态初始化必须一起完成。

//静态初始化1
元素数据类型[][] 二维数组的名称 = {
{元素1,元素2,元素3...},
{第二行的值列表 ...},
...
{第n行的值列表 ...}
};

//静态初始化2
元素的数据类型[][] 二维数组名 = new 元素数据类型[][]{
{元素1,元素2,元素3...},
{ },
{ }
};

//静态初始化3
元素的数据类型[][] 二维数组名;
二维数组名 = new 元素的数据类型[][]{
{元素1,元素2,元素3...},
{第二行的值列表 ...},
{ }
};

如果是静态初始化,右侧的[ ] [ ]不可以写数字,行数和列数由{ }中的元素决定。

3. 二维数组的使用

  • 二维数组的长度/行数:二维数组名.length
  • 二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据。它本质上是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象。
  • 某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组。
  • 某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列。
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
//用二维数组存储3个组的每个学员的成绩
public class TwoDimensionalArrayUse {
public static void main(String[] args) {

//声明并初始化二维数组array
int[][] score = {
{80,60,90,50,40},
{90,65,95,35,45},
{20,30,40,50}
};

//输出行数
System.out.println("一共有" + score.length+1 + "组");//输出行数

//输出每个组有多少个成员,即每行有多少列
for(int i = 0;i<score.length;i++){
System.out.println("第" + i + "组一共有" + score[i].length + "个学员");
}

//输出每个组的成员的成绩,即每行每列的元素。
for(int i = 0;i<score.length;i++){
System.out.print("第一组的成绩是:");
for (int j = 0;j<score[i].length-1;j++){
System.out.print(score[i][j] + ",");
}
System.out.print(score[i][score.length-1]);
System.out.println();
}

}
}

4. 二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(int i=0; i<二维数组名.length; i++){ //二维数组对象.length
for(int j=0; j<二维数组名[i].length; j++){//二维数组行对象.length
System.out.print(二维数组名[i][j]);
}
System.out.println();
}

//强化for循环遍历二维数组
for (int[] arr : arrArr) {
for (int i : arr) {
System.out.print(i + " ");
}
System.out.println();
}

5. 二维数组的动态初始化

如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。动态初始化方式分为两种格式:

(1)规则二维表:每一行的列数是相同的

1
2
3
4
5
6
7
8
9
//(1)确定行数和列数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n];
m:表示这个二维数组有多少个一维数组。或者说一共二维表有几行
n:表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格

//此时创建完数组,行数、列数确定,而且元素也都有默认值

//(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
public class TwoDimensionalArrayDynamicInit {
public static void main(String[] args) {
//确定好m行n列
int m = Integer.parseInt(args[0]);
int n = Integer.parseInt(args[1]);

//(1)声明m行n列的二维数组,声明完成后,行数列数确定,元素也都有默认值。
int[][] array = new int[m][n];

//(2)为元素赋新值。
array[0][0] = 10;

System.out.println("第1行第1列的数值是:" + array[0][0] + " 第2行2列的数值是:" + array[1][1]);//第1行第1列的数值是:10 第2行2列的数值是:0

//遍历二维数组并赋值
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
array[i][j] = i + 1;
}
}

//遍历二维数组并输出
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();
}
}
}

(2)不规则:每一行的列数不一样

1
2
3
4
5
6
7
8
9
10
11
12
//(1)先确定总行数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[总行数][];

//此时只是确定了总行数,每一行里面现在是null

//(2)再确定每一行的列数,创建每一行的一维数组
二维数组名[行下标] = new 元素的数据类型[该行的总列数];

//此时已经new完的行的元素就有默认值了,没有new的行还是null

//(3)再为元素赋值
二维数组名[行下标][列下标] = 值;
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
/**
* 动态初始化-不规则二维表-每一行列数不同
*/
//(1)声明m行二维数组,声明完成后,行数确定,列数不确定。每一行里面现在是null
int[][] array1 = new int[3][];

//(2)为每一行规定列数。创建每一行的一维数组。
array1[0] = new int[1];//二维数组名[行下标] = new 元素数据类型[该行的总列数];第1行有1列
array1[1] = new int[2];//第2行有2列
array1[2] = new int[3];//第3行有3列

//(3)为每个元素赋值
for (int i = 0; i < array1.length; i++) {
for (int j = 0; j < array1[i].length; j++) {
array1[i][j] = i+1;
}
}

//输出二维数组所有元素。
for (int i = 0;i<array1.length;i++){
for(int j = 0;j<array1[i].length;j++){
System.out.print(array1[i][j] + " ");
}
System.out.println();
}

6. 空指针异常

1
2
3
4
5
6
7
8
public class Test26NullPointerException {
public static void main(String[] args) {
//定义数组
int[][] arr = new int[3][];

System.out.println(arr[0][0]);//NullPointerException
}
}

因为此时数组的每一行还未分配具体存储元素的空间,此时arr[0]是null,此时访问arr[0][0]会抛出NullPointerException 空指针异常。

4.7 练习

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
//打印一个10行的杨辉三角
/*
1. 第一行有 1 个元素, 第 n 行有 n 个元素
2. 每一行的第一个元素和最后一个元素都是 1
3. 从第三行开始, 对于非第一个元素和最后一个元素的元素.
yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
*/
public class YangHuiTriangle {
public static void main(String[] args) {
//声明二维数组并定义行数
int[][] array = new int[10][];
//定义二维数组的列数
for (int i = 0; i < 10; i++) {
array[i] = new int[i + 1];
}

for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
if (j == 0 || i == j) {
array[i][j] = 1;
} else {
array[i][j] = array[i - 1][j - 1] + array[i - 1][j];
}
}
}

//输出杨辉三角
for(int i = 0;i<array.length;i++){
for(int j = 0;j<array[i].length;j++){
System.out.printf("%-4d",array[i][j]);
}
System.out.println();
}
}
}

4.8 可变参数

//数组形参定义方法

public static void test(int a ,String[] books);

//以可变个数形参来定义方法

public static void test(int a ,String … books);

说明

1.可变参数:方法参数部分指定类型的参数个数是可变多个

2.声明方式:方法名(参数的类型名…参数名)

3.可变参数方法的使用与方法参数部分使用数组是一致的

4.方法的参数部分有可变形参,需要放在形参声明的最后

5.每个形参列表都只能有一个可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void test(String[] msg){
System.out.println(“含字符串数组参数的test方法 ");
}
public void test1(String book){
System.out.println(“****与可变形参方法构成重载的test1方法****");
}
public void test1(String ... books){
System.out.println("****形参长度可变的test1方法****");
}
public static void main(String[] args){
TestOverload to = new TestOverload();
//下面两次调用将执行第二个test方法
to.test1();
to.test1("aa" , "bb");
//下面将执行第一个test方法
to.test(new String[]{"aa"});
}

4.9 取子数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 取子数组
*/
//先把奇数放入新数组
int count = 0;
int[] newArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
if(arr[i] % 2 != 0){
newArr[count] = arr[i];
count++;
}
}

//缩减数组
int[] newNewArr = new int[count];//如果count为空,则表示空数组
for (int i = 0; i < newNewArr.length; i++) {
newNewArr[i] = newArr[i];
}

newArr = newNewArr;

for (int i = 0; i < newArr.length; i++) {
System.out.print(newArr[i] + " ");
}

5. 面向对象编程Oriented Programming

3条主线:

1)类和成员的研究

  • 真成员(可以被继承)

    • 属性 : 描述特征
    • 方法 : 描述行为
    • 内部类 :
  • 伪成员(不能被继承)

    • 构造器 : 初始化
    • 语句块 : 初始化

2)三大特征

  • 封装 Encapsulation
  • 继承 Inheritance
  • 多态 Polymorphism

3)其他关键字

this, pacakge, import, super, static, abstract, extends, implements, interface, enum, native…

5.1 面向对象编程

1、编程语言概述

所有的计算机程序一直都是围绕着两件事在进行的,程序设计就是用某种语言编写代码来完成这两件事。

  1. 如何表示和存储数据
    • 基本数据类型的常量和变量:表示和存储一个个独立的数据
    • 对象:表示和存储与某个具体事物相关的多个数据(例如:某个学生的姓名、年龄、联系方式等)
    • 数据结构:表示和存储一组对象,数据结构有数组、链表、栈、队列、散列表、二叉树、堆……
  2. 基于这些数据都有什么操作行为,其实就是实现什么功能
    • 数据的输入和输出
    • 基于一个或两个数据的操作:赋值运算、算术运算、比较运算、逻辑运算等
    • 基于一组数据的操作:统计分析、查找最大值、查找元素、排序、遍历等

2、程序设计方法(面向过程/对象)

  1. 面向过程的程序设计思想(Process-Oriented Programming),简称POP

    • 关注的焦点是过程:过程就是操作数据的步骤,如果某个过程的实现代码在很多地方重复出现,那么就可以把这个过程抽象为一个函数,这样就可以大大简化冗余代码,也便于维护。

    • 代码结构:以函数为组织单位。独立于函数之外的数据称为全局数据,在函数内部的称为局部数据。

  2. 面向对象的程序设计思想( Object Oriented Programming),简称OOP

    • 关注的焦点是类:面向对象思想就是在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。某个事物的一个具体个体称为实例或对象。
    • 代码结构:以类为组织单位。每种事物都具备自己的属性(即表示和存储数据,在类中用成员变量表示)和行为/功能(即操作数据,在类中用成员方法表示)。

3、类和对象

1.什么是类

是一类具有相同特性的事物的抽象描述,是一组相关属性行为的集合。

  • 属性:就是该事物的状态信息。
  • 行为:就是在你这个程序中,该状态信息要做什么操作,或者基于事物的状态能做什么。

2.什么是对象

对象是一类事物的一个具体个体。即对象是类的一个实例,必然具备该类事物的属性和行为。

例如:做一个养宠物的小游戏

类:人、猫、狗等

1
2
3
4
5
6
7
8
9
10
11
12
public class Dog{
String type; //种类
String nickname; //昵称
int energy; //能量

//吃东西
public void eat(){
System.out.println("吃东西...");
energy += 10;

}
}

3.类和对象的关系

类是对一类事物的描述,是抽象的

对象是一类事物的实例,是具体的

类是对象的模板,对象是类的实体

4、如何定义类

1、类的定义格式

关键字:class

1
2
3
[修饰符] class 类名{

}
1
2
3
public class student{

}

2、对象的创建

关键字:new

1
2
3
4
类名 变量名 = new 类名()// 通过new操作符在内存中创建对象实体

//给创建的对象声明一个引用变量, 并用其保存对象地址
//通过引用中的地址值就可以多次地定位对象并使用对象了

那么,引用变量中存储的是什么呢?答:对象地址

1
2
3
4
5
6
7
8
9
10
11
public class TestStudent{
public static void main(String[] args){
System.out.println(new Student()); // 但是这里打印的并不是地址

Student stu = new Student();
System.out.println(stu);// 但是这里打印的并不是地址, 是对象的toString()方法的结果

int[] arr = new int[5];
System.out.println(arr);// 但是这里打印的并不是地址
}
}

5.2 成员变量

1、如何声明成员变量

1
2
3
[修饰符] class 类名{
[修饰符] 数据类型 成员变量名;
}

示例:

1
2
3
4
5
public class Person{
String name;
char gender;
int age;
}

位置要求:必须在类中,方法外

类型要求:可以是Java的任意类型,包括基本数据类型、引用数据类型(类、接口、数组等)

修饰符:成员变量的修饰符有很多,例如:public、protected、private、static、volatile、transient、final等

其中static可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。

2、对象的实例变量

1、实例变量的特点

(1)实例变量的值是属于某个对象的

  • 必须通过对象才能访问实例变量
  • 每个对象的实例变量的值是独立的

(2)实例变量有默认值

分类 数据类型 默认值
基本类型 整数(byte,short,int,long) 0
浮点数(float,double) 0.0
字符(char) ‘\u0000’
布尔(boolean) false
数据类型 默认值
引用类型 数组,类,接口 null

2、实例变量的访问

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
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "我";
p1.age = 18;
p1.gender = "男";

System.out.println(p1.name);
System.out.println(p1.age);
System.out.println(p1.gender);
System.out.println("-----------------");
Person p2 = new Person();

System.out.println(p2.name);
System.out.println(p2.age);
System.out.println(p2.gender);
}
}

class Person{
String name;
String gender;
int age;
}

3、实例变量的内存分析

Java对象保存在内存中时,由以下三部分组成:

  • 对象头
    • Mark Word:记录了和当前对象有关的GC、锁等信息。
    • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的,而Java对象的类数据保存在方法区,指向类的指针就是记录创建该对象的类数据在方法区的首地址。该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
    • 数组长度(只有数组对象才有)
  • 实例数据
    • 即实例变量的值
  • 对齐填充
    • 因为JVM要求Java对象占的内存大小应该是8bit的倍数,如果不满足该大小,则需要补齐至8bit的倍数,没有特别的功能。

image-20211226153433712

4、实例变量与局部变量的区别

1、声明位置和方式
(1)实例变量:在类中方法外
(2)局部变量:在方法体{}中或方法的形参列表、代码块中

2、在内存中存储的位置不同
(1)实例变量:堆
(2)局部变量:栈

3、生命周期
(1)实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被GC回收而消亡,
而且每一个对象的实例变量是独立的。
(2)局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着方法执行的结束而消亡,
而且每一次方法调用都是独立。

4、作用域
(1)实例变量:通过对象就可以使用,本类中“this.xx“,没有歧义还可以省略this.”,其他类中“对象.xx”
(2)局部变量:出了作用域就不能使用

5、修饰符
(1)实例变量:public,protected,private,final,volatile,transient等
(2)局部变量:final

6、默认值
(1)实例变量:有默认值
(2)局部变量:没有,必须手动初始化。其中的形参比较特殊,靠实参给它初始化。

5.3 封装encapsulation

1、封装概述

随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”,而“高内聚,低耦合”的体现之一:

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;
  • 低耦合:仅对外暴露少量的方法用于使用

隐藏对象内部的复杂性,只对外公开简单和可控的访问方式,从而提高系统的可扩展性、可维护性。通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

2、如何实现封装

实现封装就是指控制类或成员的可见范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

修饰符 本类 本包 其他包子类 其他包非子类
private × × ×
缺省 × ×
protected ×
public

外部类:public和缺省

成员变量、成员方法、构造器、成员内部类:public,protected,缺省,private

3、成员变量/属性私有化问题

成员变量(field)私有化之后,提供标准的get/set方法,我们把这种成员变量也称为属性(property)

get/set操作的就是事物的属性,哪怕它没有对应的成员变量。

1、成员变量封装的目的

  • 隐藏类的实现细节
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。

2、成员变量的实现步骤

  1. 使用private修饰成员变量
1
private 数据类型 变量名;

代码如下:

1
2
3
4
5
class Person {
private String name;
private int age;
private boolean marry;
}
  1. 提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:
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
class Person{
private String name;
private int age;
private boolean marry;

//设置姓名
public void setName(String sName){
name = n;
}

//获取姓名,此处使用了封装的原理,成员变量的控制修饰符private。
public String getName(){
return name;
}

//设置姓名
public void setAge(int sAge){
age = newAge;
}

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

//设置结婚
public void setMarry(boolean sMarry){
marry = sMarry;
}

//获取结婚
public boolean isMarry(){
return marry;
}
}
  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
41
42
43
44
45
46
47
48
49
50
51
public class PersonPackagingTest {
public static void main(String[] args) {
Person1 p = new Person1();

p.setName("哈哈");
p.setAge(15);
p.setMarry(true);

System.out.println(p.getAge());
System.out.println(p.getName());
System.out.println(p.isMarry());
System.out.println("---------------------");
}
}

class Person1{

private String name;
private int age;
private boolean marry;

//设置姓名
public void setName(String sName){
name = sName;
}

//获取姓名,此处使用了封装的原理,成员变量的控制修饰符private。
public String getName(){
return name;
}

//设置姓名
public void setAge(int sAge){
age = sAge;
}

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

//设置结婚
public void setMarry(boolean sMarry){
marry = sMarry;
}

//获取结婚
public boolean isMarry(){
return marry;
}
}

4、实例方法使用当前对象的成员

在实例方法中还可以使用当前对象的其他成员。在Java中当前对象用this表示。

  • this:在实例方法中,表示调用该方法的对象。
  • 如果没有歧义,完全可以省略this。

1、使用this.

案例:矩形类

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
public class ThisTest {
public static void main(String[] args) {
Rectangle rect = new Rectangle();

rect.setLength(10);
rect.setWidth(5);

System.out.println(rect.getInfo());
}
}

class Rectangle {
private int length;
private int width;

//获得长度
int getLength(){
return this.length;
}

//获得宽度
int getWidth(){
return this.width;
}

//设置长度
void setLength(int sLength){
this.length = sLength;
}

//设置宽度
void setWidth(int sWidth){
this.width = sWidth;
}

//计算面积
int area() {
return this.length * this.width;
}

//计算周长
int perimeter(){
return 2*(this.length + this.width);
}

//打印矩形
void print(char sign){
for(int i = 0;i<this.width;i++){
for(int j = 0;j<this.length;j++){
System.out.println(sign);
}
System.out.println();
}
}

//获得矩形信息
String getInfo(){
return "长:" + this.length + ",宽:" + this.width + ",面积:" + this.area() + ",周长:" + this.perimeter();
}
}

2、省略this.

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
public class ThisTest {
public static void main(String[] args) {
Rectangle rect = new Rectangle();

rect.setLength(10);
rect.setWidth(5);

System.out.println(rect.getInfo());
rect.print('*');
}
}

class Rectangle {
private int length;
private int width;

//获得长度
int getLength(){
return length;
}

//获得宽度
int getWidth(){
return width;
}

//设置长度
void setLength(int sLength){
length = sLength;
}

//设置宽度
void setWidth(int sWidth){
width = sWidth;
}

//计算面积
int area() {
return length * width;
}

//计算周长
int perimeter(){
return 2*(length + width);
}

//打印矩形
void print(char sign){
for(int i = 0;i<width;i++){
for(int j = 0;j<length;j++){
System.out.print(sign);
}
System.out.println();
}
}

//获得矩形信息
String getInfo(){
return "长:" + length + ",宽:" + width + ",面积:" + area() + ",周长:" + perimeter();
}
}

练习:

1679729205327

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
public class PackagingBoyGirl {
public static void main(String[] args) {
Girl mm = new Girl();
Boy gg = new Boy();

mm.setName("MM");
gg.setName("GG");

gg.marry(mm);
mm.marry(gg);
}
}


//设置实例Girl
class Girl{
private String name;

void setName(String i){
name = i;
}

String getName(){
return name;
}

void marry(Boy boy){
System.out.println(getName() + " Married to:" + boy.getName());
}
}

//设置实例Boy
class Boy{
private String name;
private int age;

void setName(String i){
name = i;
}

String getName(){
return name;
}

void setAge(int i){
age = i;
}

int getAge(){
return age;
}

void marry(Girl girl){
System.out.println(getName() + " Married to:" + girl.getName());
}

void shout(){

}
}

5.4 构造器(构造方法)

new完对象后,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。Java给我们提供了构造器(Constructor),直接为当前对象的某个或所有成员变量直接赋值。

1、构造器的作用

在创建对象时为对象进行初始化工作的特殊方法.

2、构造器的语法格式

构造器又称为构造方法。

首字母大写

1
2
3
4
5
6
7
8
【修饰符】 class 类名{
【修饰符】 类名(){
// 实例初始化代码
}
【修饰符】 类名(参数列表){
// 实例初始化代码
}
}

例子:

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
public class Constructor {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student("GG",18);

System.out.println(stu1.getInfo());
System.out.println(stu2.getInfo());
}
}

class Student{
String name;
int age;

//无参构造
public Student(){};

//有参构造
public Student(String name,int age){
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getInfo(){
return "姓名是:" + this.name + ",年龄是:" + this.age;
}
}

JavaBean

JavaBean是一种Java语言写成的可重用组件。

所谓javaBean,是指符合如下标准的Java类:

  • 类是公共的

  • 有一个无参的公共的构造器

  • 有属性,且有对应的get、set方法

用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

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
public class JavaBean{
private String name;
private int age;

//无参构造
public JavaBean(){};

//含参构造
public JavaBean(String name,int age){
setName(name);
}

public void setName(String name){
this.name = name;
}

public String getName(){
return name;
}

public void setAge(int age){
this.age = age;
}

public int getAge(){
return age;
}

5.5 包(package)

1、包的作用

(1)可以避免类重名:有了包之后,类的全名称就变为:包.类名

(2)可以控制某些类型或成员的可见范围

如果某个类型或者成员的权限修饰缺省的话,那么就仅限于本包使用。

(3)分类组织管理众多的类

2、声明包

关键字package

1
package 包名

注意:

(1)必须在源文件的代码首行

(2)一个源文件只能有一个声明包的package语句

包的命名规范和习惯:
(1)所有单词都小写,每一个单词之间使用.分割
(2)习惯用公司的域名倒置开头

例如:com.aierns.xxx;

3、跨包使用类

==注意:==只有public的类才能被跨包使用

(1)使用类型的全名称

例如:java.util.Scanner input = new java.util.Scanner(System.in);

(2)使用import 语句之后,代码中使用简名称

import语句告诉编译器到哪里去寻找类。

import语句的语法格式:

1
2
import 包.类名;
import 包.*;

注意:

使用java.lang包下的类,不需要import语句,就直接可以使用简名称

import语句必须在package下面,class的上面

当使用两个不同包的同名类时,例如:java.util.Date和java.sql.Date。一个使用全名称,一个使用简名称

5.6 继承(inheritance)

1、Java中的继承

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成某种关系。

多个类可以称为子类,也叫派生类;多个类抽取出来的这个类称为父类超类(superclass)或者基类

继承描述的是事物之间的所属关系,这种关系是:is-a 的关系。例如,图中猫属于动物,狗也属于动物。可见,父类更通用或更一般,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。

2、继承的好处

  • 提高代码的复用性
  • 提高代码的扩展性
  • 表示类与类之间的is-a关系

3、继承的语法格式

通过 extends 关键字,可以声明一个子类继承另外一个父类。

1
2
3
4
5
6
7
【修饰符】 class 父类 {
...
}

【修饰符】 class 子类 extends 父类 {
...
}

父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 定义动物类Animal,做为父类
*/
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;

// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"
+ name + "在吃东西");
}
}

子类

1
2
3
4
5
6
7
8
9
10
11
/*
* 定义猫类Cat 继承 动物类Animal
*/
public class AnimalCat extends Animal{
int count;//记录每只猫抓老鼠的数量

public void catchMouse(){
count++;
System.out.println("已经抓了" + count + "只老鼠");
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
public class AnimalTest {
public static void main(String[] args) {
AnimalCat cat = new AnimalCat();
cat.name = "Tom";
cat.age = 2;
cat.eat();
cat.catchMouse();
cat.catchMouse();
cat.catchMouse();
}
}

4、继承的特点

  • 子类会继承父类所有成员(构造器除外)
  • Java只支持单继承,不支持多重继承(只能继承一个)
  • Java支持多层继承(继承体系)
  • 一个父类可以同时拥有多个子类

5、IDEA中如何查看继承关系

1、子类和父类是一种相对的概念。例如:B类对于A类来说是子类,但是对于C类来说是父类。

2、查看继承父系快捷键

选择A类名,按Ctrl+H就会显示A类的继承树。

例如:在类继承目录树中选中某个类,比如C类,按Ctrl+ Alt+U就会用图形化方式显示C类的继承祖宗。

6、权限修饰符限制问题

  • 外部类要跨包使用必须是public,否则仅限于本包使用
  • 成员的权限修饰符问题

(1)本包下使用:成员的权限修饰符可以是public、protected、缺省

(2)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义

  • 父类成员变量私有化

子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。

7、继承注解

1
2
* @Override // 注解(annotation) : 是特殊的注释, 特殊在于可以被编译器和JVM识别.
* @Override此注解的作用就是提醒编译器, 这个方法要重写父类方法了, 请帮助我作条件检查. 如果有问题, 请编译出错

8、this与super的区别

1
2
* this : 表示方法的调用者对象的引用
* super : 表示本类的父类中继承来的成员的限定, 并不是父类对象
1
2
* this.属性 和 this.方法() 都有追溯性, 优先从本类中开始查找, 如果本类没有, 向上追溯
* super.属性 和 super.方法() 也有追溯性, 先从直接父类开始查找, 向上追溯.

9、子类构造器

  • 子类中所有的构造器默认都会调用父类中空参数的构造器

  • 子类构造器中的第1行, 默认就是super(); 作用就是调用父类构造器. 它必须在第一行, 达到了先调用父类构造器的效果.

  • 子类构造器中的第1行一定要么是super(…)要么是 this(…)

  • super(…) 直接调用父类构造器,this(…) 间接调用父类构造器

    结论 :

    •  子类构造器中的第一行必须要有对父类构造器的调用. 第一行的作用是保证先父后子.
      

5.7 方法重写(Override)

父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于子类,该怎么办呢?我们可以进行方法重写 (Override)

1、方法重写

1
2
3
4
5
6
7
8
9
10
11
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
1
2
3
4
5
6
7
8
9
//smartphone:智能手机
public class Smartphone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
Smartphone sp = new Smartphone();

// 调用父类继承而来的方法
sp.call();

// 调用子类重写的方法
sp.showNum();
}
}

2、在子类中如何调用父类被重写的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.inherited.method;

//smartphone:智能手机
public class Smartphone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");

//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}

3、IDEA重写方法快捷键:Ctrl+O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//smartphone:智能手机
public class Smartphone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");

//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}

@Override
public void call() {
super.call();
System.out.println("视频通话");
}
}

@Override:写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

4、重写方法的要求

1.必须保证父子类之间重写方法的方法签名必须一致(返回值类型 方法名, 参数列表)。

注意:如果返回值类型是基本数据类型和void,那么必须是相同.如果返回值类型是引用型时,子类重写方法可以返回父类方法的返回值的子类类型.

2.子类方法的权限必须【大于等于】父类方法的权限修饰符。

注意:public > protected > 缺省 > private

父类私有方法不能重写

跨包的父类缺省的方法也不能重写

3.父类不能被static,final,private修饰

4.子类重写方法抛出的受检异常异常类型要小于等于父类方法抛出的受检异常.

5、方法的重载和方法的重写

方法的重载:方法名相同,形参列表不同。不看返回值类型。

方法的重写:必须保证父子类之间重写方法的方法签名必须一致(返回值类型 方法名, 参数列表)

(1)同一个类中

1
2
3
4
5
6
7
8
9
10
11
public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}

(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
public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法

Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}

class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}

5.8 多态(polymorphism)

多态是继封装、继承之后,面向对象的第三大特性。

跑的动作,小猫、小狗和大象,跑起来是不一样的。可见,同一行为,对于不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。

子类对象有多种父类形态,把子类对象作为父类对象使用.

1、多态解决什么样的问题

案例:

(1)声明一个Dog类,包含public void eat()方法,输出“狗狗啃骨头”

(2)声明一个Cat类,包含public void eat()方法,输出“猫咪吃鱼仔”

(3)声明一个Person类,

  • 包含宠物属性
  • 包含领养宠物方法 public void adopt(宠物类型 pet)
  • 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
1
2
3
4
5
public class Dog {
public void eat(){
System.out.println("狗狗啃骨头");
}
}
1
2
3
4
5
public class Cat {
public void eat(){
System.out.println("猫咪吃鱼仔");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person {
private Dog dog;

//adopt:领养
public void adopt(Dog dog){
this.dog = dog;
}

//feed:喂食
public void feed(){
if(dog != null){
dog.eat();
}
}
/*
问题:
1、从养狗切换到养猫怎么办?
修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是同时养多个狗,或猫怎么办?
4、要是还有更多其他宠物类型怎么办?
如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
*/
}

2、多态的形式和体现

1. 多态引用

Java规定父类类型的变量可以接收子类类型的对象。

1
父类类型 变量名 = 子类对象;

父类类型:指子类继承的父类类型,或者实现的父接口类型。

所以说继承是多态的前提

2. 多态引用的表现

表现:编译时类型与运行时类型不一致,编译时看“父类”,运行时看“子类”。

3. 多态引用的好处与弊端

弊端:编译时,只能调用父类声明的方法,不能调用子类扩展的方法;

好处:运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

4. 多态演示

让Dog和Cat都继承Pet宠物类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Pet {
private String nickname;

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public void eat(){
System.out.println(nickname + "吃东西");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Cat extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}

//子类扩展的方法
public void catchMouse() {
System.out.println("抓老鼠");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Dog extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("狗狗" + getNickname() + "啃骨头");
}

//子类扩展的方法
public void watchHouse() {
System.out.println("看家");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");

//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法

pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}

3、应用多态解决问题

  1. 声明变量是父类类型,变量赋值子类对象
  • 方法的形参是父类类型,调用方法的实参是子类对象
  • 实例变量声明父类类型,实际存储的是子类对象
1
2
3
4
5
6
7
8
9
public class OnePersonOnePet {
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象.
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestOnePersonOnePet {
public static void main(String[] args) {
OnePersonOnePet person = new OnePersonOnePet();

Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();

Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
  1. 数组元素是父类类型,元素对象是子类对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class OnePersonManyPets {
    private Pet[] pets;//数组元素类型是父类类型,元素存储的是子类对象

    public void adopt(Pet[] pets) {
    this.pets = pets;
    }

    public void feed() {
    for (int i = 0; i < pets.length; i++) {
    pets[i].eat();//pets[i]实际引用的对象类型不同,执行的eat方法也不同
    }
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestPets {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");

OnePersonManyPets person = new OnePersonManyPets();
person.adopt(pets);
person.feed();
}
}
  1. 方法返回值类型声明为父类类型,实际返回的是子类对象
1
2
3
4
5
6
7
8
9
10
11
12
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();

Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();

Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}

4、向上转型与向下转型

对象的类型转换是和基本数据类型的转换是不同的。基本数据类型是把数据值copy了一份,相当于有两种数据类型的值。而对象的赋值不会产生两个对象。

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点”小麻烦”。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过

向上转型:

​ 左父右子 ,向上转型(自动完成).

  • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
  • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
  • 此时,一定是安全的,而且也是自动完成的

向下转型:

​ 左子右父,向下转型: (子类类型)父类变量

  • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
  • 但是,运行时,仍然是对象本身的类型
  • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog

//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse

Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse

Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}

5、instanceof关键字

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。

1
变量/匿名对象 instanceof 数据类型 

那么,哪些instanceof判断会返回true呢?

  • 变量/匿名对象的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
  • 变量/匿名对象的运行时类型<= instanceof后面数据类型,才为true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");

for (int i = 0; i < pets.length; i++) {
pets[i].eat();

if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}

6、虚方法

指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

当我们通过“对象xx.方法”的形式调用一个虚方法时,要如何确定它具体执行哪个方法呢?

(1)静态分派:先看这个对象xx的编译时类型,在这个对象的编译时类型中找到能匹配的方法

1
2
3
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型

(2)动态绑定:再看这个对象xx的运行时类型,如果这个对象xx的运行时类重写了刚刚找到的那个匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个匹配的方法

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
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Father d) {
System.out.println("sub--father");
}
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{

}
class Son extends Father{

}
class Daughter extends Father{

}
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
public class TestVirtualMethod {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);//sub--
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参f的编译时类型是Father,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
my.method(s);//son
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参s的编译时类型是Son,形参(Father f) 、(Son s)
最匹配的是public void method(Son s)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Son s)进行重写
发现没有重写,如果没有重写,就执行刚刚父类中找到的方法
*/
my.method(d);//sub--
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参d的编译时类型是Daughter,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
}
}

7、成员变量没有多态一说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//成员变量没有多态一说
public class TestVariable {
public static void main(String[] args) {
Base b = new Sub();
System.out.println(b.a);//a = 1
System.out.println(((Sub)b).a);//a = 2

Sub s = new Sub();
System.out.println(s.a);//a = 2
System.out.println(((Base)s).a);//a = 1;
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}

8、什么具有多态性

只有非静态方法具有多态性,其他的都不具有.

5.9 权限访问修饰符

修饰符 作用对象
private 属性\方法
default 属性\方法\类\接口
protected 属性\方法
public 属性\方法\类\接口

即private和protected不能修饰类\接口

范围 private default protected public
同包同类
同包不同类
不同包的子类
不同包的非子类

protected主要用于继承,如果是不同包,需要是父类的子类才可以访问到protected修饰的属性或方法

5.10 实例初始化

  • 子类继承父类时,不会继承父类的构造器。只能通过super()或super(实参列表)的方式调用父类的构造器。
  • super();:子类构造器中一定会调用父类的构造器,默认调用父类的无参构造,super();可以省略。
  • super(实参列表);:如果父类没有无参构造或者有无参构造但是子类就是想要调用父类的有参构造,则必须使用super(实参列表);的语句。
  • super()和super(实参列表)都只能出现在子类构造器的首行

初始化相关代码

(1) 实例变量直接初始化

(2) 非静态代码块

(3) 构造器

5.11 关键字和API

1. this和super关键字

this: 当前方法的调用者对象

super: 引用父类声明的成员

1. this和super的使用格式
  • this
    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
    • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super
    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
2. 避免子类和父类声明重名的成员变量

因为,子类会继承父类所有的成员变量,所以:

  • 如果重名的成员变量表示相同的意义,就无需重复声明
  • 如果重名的成员变量表示不同的意义,会引起歧义
3. 解决成员变量重名问题
  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量(==就近原则==)
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
  • 变量前面没有super.和this.

    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的==局部变量==,
    • 如果不是局部变量,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量==(权限修饰符允许在子类中访问的)
  • 变量前面有this.

    • 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
  • 变量前面super.

    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
4. 解决成员方法重写后调用问题
  • 如果子类没有重写父类的方法,只有权限修饰符运行,在子类中完全可以直接调用父类的方法;
  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

2. native关键字

​ native只能修饰方法,表示这个方法的方法体代码不是用Java语言实现的,而是由C/C++语言编写的。

3. final关键字

1. final修饰类

​ 被final修饰的是终极类,不能被继承,没有子类

2. final修饰方法

​ 表示这个方法不能被子类重写

3. final修饰变量

​ final修饰某个变量(成员变量或局部变量),表示它的值就不能被修改,即常量,常量名建议使用大写字母。

5.12 Object根父类

java.lang.Object是类层次结构的根类,即所有类的父类。每个类都使用 Object 作为超类。

1. Object类的其中5个方法

  • toString()

方法签名 : public String toString();

直接使用System.out.println(对象),默认会自动调用

  • getClass()

方法签名 : public final Class<?> getClass();

  • equals()

方法签名 : public boolean equals(Object obj) : 用于判断当前对象this与指定对象obj是否“相等”

自反性: x.equals(x)返回true

传递性: x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true

一致性: 只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致

对称性: x.equals(y)与y.equals(x)结果应该一样

非空对象与null的equals一定是false

  • hashCode()

public int hashCode() : 返回每个对象的hash值

==如果重写equals,那么通常会一起重写hashCode()方法==

①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;

②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;

③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false;

  • finalize()

protected void finalize():用于最终清理内存的方法

每一个对象的finalize()只会被调用一次,哪怕它多次被标记为垃圾对象。

面试题:对finalize()的理解?

  • 当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法,不是由程序员手动调用。

  • 这个方法与C语言的析构函数不同,C语言的析构函数被调用,那么对象一定被销毁,内存被回收,而finalize方法的调用不一定会销毁当前对象,因为可能在finalize()中出现了让当前对象“复活”的代码

  • 每一个对象的finalize方法只会被调用一次。

  • 子类可以选择重写,一般用于彻底释放一些资源对象,而且这些资源对象往往时通过C/C++等代码申请的资源内存

5.13 对象关联

1
2
3
4
5
6
* 对象关联: 一个对象拥有另一个对象,为了让一个对象更方便的使用另一个对象
* 如何关联: 把要拥有或者关联的对象作为我的属性
* 一旦关联了对象:
* (1)全参构造
* (2)提供get/set方法
* (3)改造say方法

5.14 静态Static

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
static可以修饰属性,方法,代码块,内部类

static修饰属性:
1.变量的分类(按位置分):成员变量(属性) vs 局部变量
成员变量的分类 :实例变量(不使用static修饰) vs 类变量(用static修饰)

2.同一个类的多个对象共同拥有一份类变量。每个对象各自拥有一份实例变量。
当该类的某个对象对类变量进行修改那么其它对象看到的是修改后的内容(类变量是共享的)。

3.类变量是随着类的加载而加载,类变量在方法区。
实例变量是随着对象的创建进行加载在堆中。
类加载只加载一次。类加载优先于创建对象。

4.调用类变量:对象名.类变量名。
类名.类变量名

static修饰方法:
1.static修饰方法叫作静态方法(类方法)。
2.静态方法是随着类的加载而加载的。
3.调用静态方法:静态方法(类方法)
4.非静态方法可以调用类变量,实例变量,非静态方法和类方法
静态方法中不能调用实例变量和非静态方法因为加载时机不同。
5.在静态方法中是否可以使用thissuper关键字?不可以

使用场景:
静态方法:
工具类(提供一些具体功能的常用方法的类)中的方法都是静态方法。
类变量:
常量:public static final double PI = 3.14159265358979323846;
如果多个对象拥要共享属性就需要用到类变量(比如:银行利率)。



1.子类继承父类中的类变量和静态方法了吗?继承到了
2.子类可以重写父类中的静态方法吗?不可以
3.为什么不可以重写父类的静态方法?
多态 :继承关系 方法的重写 (可以这么理解方法的重写就是为多态做铺垫)
多态:多态是动态绑定(在程序的运行的时候才知道调用的是谁的方法)
多态:父类的引用指向子类的对象 ----重点是子类的对象(静态方法不需对象)

静态方法:静态绑定-在编译的时候就决定了是哪个方法

5.15 抽象类

1
2
3
4
5
6
7
8
9
抽象类和抽象方法(abstract关键字)

abstract修饰类:抽象类
1.抽象类是不能被实例化的
2.抽象类如何使用:只能让子类继承抽象类(只要有抽象类必用多态)
3.子类一旦继承父类(抽象类)那么该子类必须实现父类中所有的抽象方法
注意:①子类也为抽象类那么不需要实现父类的抽象方法。
②如果父类(抽象类)重写了父类的父类(抽象类)那么子类(不是抽象类)可以不用实现该方法。

1
2
3
4
5
6
7
8
1.子类继承父类中的类变量和静态方法了吗?继承到了
2.子类可以重写父类中的静态方法吗?不可以
3.为什么不可以重写父类的静态方法?
多态 :继承关系 方法的重写 (可以这么理解方法的重写就是为多态做铺垫)
多态:多态是动态绑定(在程序的运行的时候才知道调用的是谁的方法)
多态:父类的引用指向子类的对象 ----重点是子类的对象(静态方法不需对象)

静态方法:静态绑定-在编译的时候就决定了是哪个方法

5.16 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
具体的类:对事物的描述
抽象的类:对同一类不同事物共性的描述
接口:不同的类不同的事物相同功能的描述

接口的格式:
权限修饰符(public和缺省的) interface 接口名{
}

说明:
1.接口和类是并列的关系。
2.接口中有
JDK1.8之前: 常量(public static final int AGE = 18)
抽象方法
JDK1.8之后: 常量,抽象方法,默认方法(default关键字修饰的方法),静态方法
3.接口中没有构造器不能创建实例。
4.怎么用接口 :
类和接口之间有的关系-实现关系(多实现):类 implements 接口1,接口2,.....
类和类的关系-继承关系而且是单继承
接口和接口的关系-继承关系而且是多继承
5.接口的多态性:接口的类型指向实现类的对象

5.17 内部类

5.18 枚举

5.19 注解

5.20 包装类

6. 枚举

7. 异常

8. 常用类

9. 集合

10. 迭代器

11. 泛型

12. IO流