01 Java语言操作
人机交互方式:
- 图形化界面(Graphical User Interface,GUI)
- 命令行方式(Command Line Interface, CLI)
- 常用DOS命令
- dir: 列出当前目录下的文件以及文件夹
- md:创建目录
- rd:删除目录
- cd:进入指定目录
- cd…:退回到上一级目录
- cd\:回到根目录
- del:删除文件
- exit:推出dos命令行
- echo [content]>[name]:创建name文件并将content写入文件
- 常用DOS命令
计算机编程语言:
- 第一代语言:机器语言
- 第二代语言:汇编语言。使用助记符表示一条机器指令。
- 第三代语言:高级语言
- C/Pascal/Fortran面向过程的语言
- C++面向过程/面向对象
- Java跨平台的纯面向对象的语言
- .NET跨语言的平台
- Python/Scala…
Java语言的特点:
- 面向对象
- 两个基本概念: 类、对象
- 三大特性:封装、继承、多态
- 健壮性
- 吸收了C/C++语言的有点,但去掉了其影响程序健壮性的部分(如指针、内存的申请与释放等),提供了一个相对安全的内存管理和访问机制。
- 跨平台性
- 原理:只要在需要运行java应用程序的操作系统上,先安装一个Java虚拟机(JVM)即可。
Java注释类型
- 单行注释
- 多行注释
- 文档注释(java特有):
- 文档注释内容可以被JDK提供的工具javadoc所解析,生成一套以网页文件形式体现的该程序的说明文档
/**
@author 指定java程序的作者
@version 指定源文件的版本
*/
javadoc -d myjava -author -version JavaComment.java
Java API文档
- API(Application Programming Interface,应用程序编程接口)是java提供的基本编程接口
- Java语言提供了大量的基础类,因此Oracle也为这些基础类提供了相应的API文档,用于告诉开发者如何使用这些类,以及这些类包含的方法
java程序编写-编译-运行的过程
- 编写:我们将编写的java代码保存在以".java"结尾的源文件中
- 编译:使用javac.exe命令编译我们的java源文件。格式:javac 源文件名.java
- 运行:使用java.exe命令解释已编译的字节码文件。格式: java 类名
注意事项
- 在一个java源文件中可以声明多个class。但是,只能最多有一个类声明为public,并且public类须与源文件同名
- 每一个执行语句都以";"结束
hellojava.java
- 程序的入口是main()方法。格式是固定的。
- 输出语句:
- System.out.println(); 先输出数据,然后换行
- System.out.print(); 不换行
02 基本语法
2-1 关键字和保留字
keyword
a. 用于定义数据类型的关键字 |
---|
class |
interface |
enum |
byte |
short |
int |
long |
float |
double |
char |
boolean |
void |
b. 用于定义流程控制的关键字 |
---|
if |
else |
switch |
case |
default |
while |
do |
for |
break |
continue |
return |
c. 用于定义访问权限修饰符的关键字 |
---|
private |
protected |
public |
d. 用于定义类、函数、变量修饰符的关键字 |
---|
abstract |
final |
static |
synchronized |
e. 用于定义类与类之间关系的关键字 |
---|
extends |
implements |
f. 用于定义建立实例以及引用实例、判断实例的关键字 |
---|
new |
this |
super |
instanceof |
g. 用于异常处理的关键字 |
---|
try |
catch |
finally |
throw |
throws |
h. 用于包的关键字 |
---|
package |
import |
i. 其他修饰符关键字 |
---|
native |
strictfp |
transient |
volatile |
assert |
reserved word:保留字
现有java版本尚未使用,但以后可能会作为关键字使用。
- goto
- const
2-2 标识符 (identifier)
定义:java对各种变量、方法和类等要素命名时使用的字符序列。
技巧:凡是自己可以起名字的地方都叫标识符
定义合法标识符规则
- 由英文字母大小写,0-9,_或$组成
- 数字不可以开头
- 不可以使用(但可以包含)关键字和保留字
- Java中严格区分大小写,长度无限制
- 不能包含空格
Java中名称命名规范
- 注意:命名时尽量能够见名知意
类型 | 说明 | 示例 |
---|---|---|
包名 | 多单词组成时所有字母都小写 | xxxyyy |
类名、接口名 | 多单词组成时,所有单词的首字母大写 | XxxYyyZzz |
变量名、方法名 | 多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写 | xxxYyyZzz |
常量名 | 所有字母都大写,多单词时每个单词用下划线连接 | XXX_YYY_ZZZ |
2-3 变量
变量的概念
- 内存中的一个存储区域
- 该区域的数据可以在同一类型范围内不断变化
- 变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值。
变量的作用
- 用于在内存中保存数据
使用变量的注意事项
- Java中每个变量必须先声明后使用
- 使用变量名来访问这块区域的数据
- 变量只有在其作用域内才有效
- 同一个作用域内,不能定义重名的变量
基本数据类型
- java对于每一种数据都定义了明确的具体数据类型(强类型语言),在内存中分配了不同大小的内存空间。
整型
类型 | 占用存储空间 | 表数范围 |
---|---|---|
byte | 1字节=8bit位 | -128~127 |
short | 2字节 | [ − 2 15 , 2 15 − 1 ] [-2^{15},2^{15}-1] [−215,215−1] |
int | 4字节 | [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1] [−231,231−1] |
long | 8字节 | [ − 2 63 , 2 63 − 1 ] [-2^{63},2^{63}-1] [−263,263−1] |
浮点型
类型 | 占用存储空间 | 表数范围 |
---|---|---|
单精度float | 4字节 | |
双精度double | 8字节 |
字符型
- char(1字符=2字节)
- 定义char变量,通常使用一对’’,内部只能写一个字符
- 表示方式:
- 声明一个字符
- 转义字符\n
- 制表符\t
- 直接使用Unicode值来表示字符型常量,’\uXXXX’,XXXX该表一个十六进制整数。例如\u000a表示\n
注意事项:
- 通常定义整型变量时用int,定义浮点型变量时用double
- long量末尾必须以L/l结尾
- float变量末尾必须以F/f结尾
- 整型常量,默认类型为int
- 浮点型常量,默认类型为double型
布尔型boolean
基本数据类型与String间转换
- String不是基本数据类型,属于引用数据类型
- 声明String变量类型时,使用一对双引号
- String可以和8种基本数据类型做运算,且只能是连接运算“+”,运算结果仍然是String
自动类型提升
- 当容量小(指表数范围)的数据类型变量与容量大的数据类型变量做运算时,结果自动提升为容量大的数据类型
- byte, short, char之间做运算,结果自动提升为int型
(byte, short, char) -> int -> long -> float -> double
强制类型转换
- 需要使用强转符
- 可能出现精度损失
class VariableTest3{
public static void main(String[] args){
double d1 = 12.9;
//使用强转符
int i1 = (int)d1;//12(截断操作而不是四舍五入)
System.out.println(i1);
}
}
进制与进制间的转换
进制 | 说明 |
---|---|
二进制(binary) | 以0b或0B开头 |
十进制(decimal) | |
八进制(octal) | 以数字0开头 |
十六进制(hex) | 以0x或0X开头 |
二进制
-
Java整数常量默认是int型,当用二进制定义整数时,其第32位是符号位;而long型时,二进制默认占64位,第64位是符号位
-
二进制的整数有如下三种形式:
- 原码:直接将一个数值换为二进制数。最高位是符号位。
- 负数的反码:是对原码按位取反,最高位符号位确定为1
- 负数的补码:其反码+1
-
计算机以二进制补码的形式保存所有整数
- 正数的原码、反码、补码都相同
- 负数的补码是其反码+1
decimal | 名称 | bit1(符号位) | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 | bit8 |
---|---|---|---|---|---|---|---|---|---|
14 | 原码/反码/补码 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
-14 | 原码 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
-14 | 反码 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
-14 | 补码 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 |
二进制与十进制互转
二进制与八进制互转
- 2>8:低到高三个一位
- 8>2:一位拆三位
二进制与十六进制互转
- 2>16:从低到高四个一位
- 16>2:一位拆四位
2-4 运算符
a. 算术运算符
运算符 | 说明 | 范例 | 结果 |
---|---|---|---|
+ | 正号 | ||
- | 负号 | ||
+ | |||
- | |||
* | |||
/ | |||
% | 求模 | 7%5 | 2 |
++( ) | 先运算后取值 | a=2;b=++a | a=3;b=3 |
( )++ | 先取值后运算 | a=2;b=a++ | a=3;b=2 |
–( ) | 先运算后取值 | a=2;b=–a | a=1;b=1 |
( )– | 先取值后运算 | a=2;b=a– | a=1;b=2 |
+ | 字符串连接 |
b. 赋值运算符
- 等号:=
- +=
- -=
- *=
- /=
- %=
c. 比较运算符(关系运算符)
- ==
- !=
- <
- >
- <=
- >=
- instanceof: 检查是否是类的对象
"Hello" instance of String //true
d. 逻辑运算符
符号 | 说明 |
---|---|
& | 逻辑与 |
&& | 短路与 |
| | 逻辑或 |
|| | 短路或 |
! | 逻辑非 |
^ | 逻辑异或 |
实例
a | b | a&b | a&&b | a|b | a||b | !a | a^b |
---|---|---|---|---|---|---|---|
true | true | true | true | true | false | false | |
true | false | false | false | true | true | false | true |
false | true | false | false | true | true | true | true |
false | false | false | false | false | false | true | false |
注意:开发中推荐使用短路与和短路或
e. 位运算符
位运算是直接对整数的二进制进行的运算。
运算符 | 运算 | 示例 | 说明 | 细节 |
---|---|---|---|---|
<< | 左移 | 3 << 2 = 12 | 3*2*2=12 | 空缺位补0 |
>> | 右移 | 3 >> 1 = 1 | 3/2=1 | 最高位是0,空缺位补0;最高位是1,空缺位补1 |
>>> | 无符号右移 | 3 >>> 1 = 1 | 3/2=1 | 空缺位补0 |
& | 与运算 | 6 & 3 = 2 | ||
| | 或运算 | 6 | 3 = 7 | |
^ | 异或运算 | 6 ^ 3 = 5 | a ^ b ^ a = b | |
~ | 取反运算 | ~6 = -7 | 各二进制码按补码各位取反 |
异或运算的应用举例:交换两个数的值
class AriTest{
public static void main(String[] args){
int num1 = 10;
int num2 = 20;
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
//方法一:取值暂存
int temp = num1;
num1 = num2;
num2 = temp;
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
//方式2:利用位运算的特点(只适用于数值类型)
num1 = num1 ^ num2;
num2 = num1 ^ num2;//num2 <- num1
num1 = num1 ^ num2;//num1 <- num2
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
}
}
f. 三元运算符
- 结构: (条件表达式)?表达式1: 表达式2;
- 说明
** 条件表达式的结果位Boolean类型
** 如果表达式为true,则执行表达式1;否则执行表达式2
** 表达式1和表达式2要求是一致的类型
** 三元运算符可以嵌套使用 - 凡是可以使用三元运算符的地方,都可以改写成if…else…;反过来不成立
- 如果程序既可以使用三元运算符,又可以使用if…else…,则选择三元运算符
运算符的优先级(略)
2-5 程序流程控制
I. 顺序结构
程序从上到下逐行执行,中间没有任何判断和跳转。
II. 分支结构
根据条件,选择性地执行某段代码。
if…else
switch-case
switch(表达式){
case 常量1:
语句1;
//break;
case 常量2:
语句2;
//break;
......
case 常量N:
语句N;
//break;
default:
语句;
//break
}
说明
-
根据switch表达式中的值,一次匹配各个case中的常量。一旦匹配成功,则进入相应case执行语句
-
当调用完当前case执行语句后,则继续不加判断向下执行其他case结构中的执行语句,直至break或结束
-
要想实现多选一结构,需要在每个case执行语句后面加break关键字
-
switch中的表达式只能是如下六种类型之一:byte short char int 枚举类型 String
-
case表达式只能是一个常量,不能是一个范围或boolean
-
default相当于if-else中的else,default结构也是可选的,而且位置是灵活的
III. 循环结构
循环语句的四个组成部分
- 初始化部分(init_statement)
- 循环条件部分(test_exp)
- 循环体部分(body_statement)
- 迭代部分(alter_statement)
循环的次数
- 不在循环条件部分限制次数的结构:for(;;)或while(true)
- 结束循环有几种方式?
方式一:循环条件部分返回false
方式二:在循环体中执行break
A. for循环
for(1;2;4){
3
}
例题
编写程序从1循环到150,并在每行打印一个值,另外在每个3的倍数行上打印出“foo”,在每个5的倍数行上打印“biz”,在每个7的倍数行上打印“baz”。
class ForExer {
public static void main(String[] args) {
for(int i = 1; i <=150; i++){
System.out.print(i);
if(i % 3 == 0){
System.out.print(" foo");
}
if(i % 5 == 0){
System.out.print(" biz");
}
if(i % 7 ==0){
System.out.print(" baz");
}
System.out.print("\n");
}
}
}
B. while循环
1
while(2){
3;
4;
}
//执行过程:1 --> 2 --> 3 --> 4 --> 2 --> ... --> 2(False)
说明
1.写while循环千万要小心不要丢了迭代条件
2.我们写程序要避免出现死循环
3.for循环和while循环可以相互转换(区别:初始化条件的作用域不一样)
C. do…while循环
1
do{
3;
4;
}while(2);
//执行过程:1 --> 3 --> 4 --> 2 --> 3 -->...--> 2
说明
1.do-while循环至少会执行一次循环体
2.开发中执行while更多一些,do-while会比较少
嵌套循环
- 实现九九乘法表
class NineNineTest{
public static void main(String[] args){
int m;
for(int i = 1; i <= 9; i++){
for(int j = 1; j <= i; j++){
m = i * j;
System.out.print(i + "*" + j + "=" + m + " ");
}
System.out.println();
}
}
}
- 输出10000以内所有的质数(需注意算法效率问题)
class PrimeTest2 {
public static void main(String[] args) {
long start = System.currentTimeMillis();
boolean isFlag;
int primeCount = 0;
for(int i = 2; i <= 100000; i++){
isFlag = true;
for(int j = 2; j * j <= i; j++){//优化二:取开方数
if(i % j == 0){
isFlag = false;
break;//优化一:只对本身非质数的自然数是有效的
}
}
if(isFlag == true){
// System.out.println(i);
primeCount++;
}
}
long end = System.currentTimeMillis();
System.out.println("质数的个数为:" + primeCount);
System.out.println("所花费的时间为:" + (end - start));//1207
}
}
break/continue关键字
关键字 | 使用范围 | 循环中的作用 | 相同点 |
---|---|---|---|
break | ① switch-case; ② 循环结构 | 结束当前循环 | 同一作用域内关键字之后不能声明执行语句 |
continue | 循环结构中 | 结束当次循环 | 同一作用域内关键字之后不能声明执行语句 |
03 数组
3-1 数组的概述
数组(Array),是多个相同类型数据按一定顺寻排列的集合,并使用一个名字命名,通过编号的方式对这些数据进行统一管理。
数组的常见概念:
- 数组名
- 下标或索引
- 元素
- 数组的长度
数组的特点
- 数组属于引用数据类型的变量;数组的元素既可以是基本数据类型,也可以是引用数据类型。
- 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
- 数组的长度一旦确定,就不能修改。
数组的分类
- 按照维度分类:一维数组、二维数组、……
- 按照数组元素类型分类:基本数据类型元素的数组、引用数据类型元素的数组
3-2 数组的基本使用
一维数组
1. 声明和初始化
- 静态初始化: 数组的初始化和数组元素的赋值操作同时进行
- 动态初始化: 数组的初始化和数组元素的赋值操作分开进行
总结:数组一旦初始化完成,其长度就确定了
调用数组的指定位置的元素
- 通过下标的方式调用
- java中索引从0开始,到数组长度-1结束
获取数组长度
遍历数组
2. 数组元素的默认初始化值
数据类型 | 初始化值 |
---|---|
整型(byte short int long) | 0 |
浮点型(double float) | 0.0 |
char型 | 0 (ascii码0)或’\u0000’ |
boolean型 | false |
引用数据类型 | null |
3. 数组的内存解析
内存的简化结构:
- 栈 (stack):主要存放局部变量
- 堆 (heap):主要存放new出来的结构
- 对象
- 数组
- 方法区 (method area)
- 常量池
- 静态域
一维数组的内存解析:
class test {
public static void main(String[] args){
int[] arr = new it[]{1,2,3};
//左边arr放入栈中;右边数组放入堆中
//Stack:
// arr: 0x34ab
//heap:
//0x34ab:| 1 | 2 | 3 |
String[] arr1 = new String[4];
//Stack:
// arr1: 0x12ab
//heap:
//0x12ab:| null | null | null | null |
arr1[1]="刘德华";
//Stack:
// arr1: 指向地址
//heap
// 第二个元素修改为刘德华
//0x12ab:| null | 刘德华 | null | null |
arr1 = new String[3];
//Stack:
// arr1: 覆盖原地址
// arr1: 0x5566
//heap
// 0x5566: | null | null | null |
}
}
二维数组
二维数组可以看成是一维数组array1作为另一个一维数组array2的元素而存在。
其实,从数组底层的运行机制来看,没有多维数组。
1. 二维数组的声明和初始化
- 静态初始化: 数组的初始化和数组元素的赋值操作同时进行
- 动态初始化: 数组的初始化和数组元素的赋值操作分开进行
调用数组指定位置的元素
获取数组长度
遍历数组
2. 数组元素的默认初始化值
- 对于初始化类型1:String[][] arr2 = new String[3][5];
- 外层元素:地址值
- 内层元素:与一维数组初始化时相同
- 对于初始化类型2:int[][] arr3 = new int[3][];
- 外层元素:null (因为数组本身就属于一种引用类型)
- 内层元素:不能调用,否则报错NullPointerException
3. 数组的内存解析
class test {
public static void main(String[] args){
int[][] arr1 = new int[4][];
//Stack:
// arr1: 0x1234
//heap:
// 0x1234: | null | null | null | null |
arr1[1] = new int[]{1,2,3};
//Stack:
// arr1: 0x1234
//heap:
// 0x1234: | null | 0x7788 | null | null |
// 0x7788: | 1 | 2 | 3 |
arr1[2] = new int[4];
//Stack:
// arr1: 0x1234
//heap:
// 0x1234: | null | 0x7788 | 0x8374 | null |
// 0x7788: | 1 | 2 | 3 |
// 0x8374: | 0 | 0 | 0 |
arr1[2][1] = 30;
//Stack:
// arr1: 0x1234
//heap:
// 0x1234: | null | 0x7788 | 0x8374 | null |
// 0x7788: | 1 | 2 | 3 |
// 0x8374: | 0 | 30 | 0 |
}
}
3-3 数组中涉及的常见算法
- 数组元素的赋值(杨辉三角、回形数等)
- 求数值型数组中元素的最大值、最小值、平均数、总和等
- 数组的复制、反转、查找(线性查找、二分法查找)
- 数组元素的排序算法
算法的五大特征
- 输入(Input)
- 输出(Output)
- 有穷性(有限性, finiteness)
- 确定性(明确性, Definiteness)
- 可行性(有效性, Effectiveness)
A. 排序算法
通常来说,排序的目的是快速查找。
衡量排序算法的优劣:
- 时间复杂度(高效率):分析关键字的比较次数和记录的移动次数
- 空间复杂度(低存储):分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
排序算法分类:内部排序和外部排序
- 内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。
- 外部排序:
- 参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器。
- 外部排序最常见的是多路归并排序
- 可以认为外部排序是由多次内部排序组成
十大内部排序算法
- 选择排序
- 直接选择排序
- 堆排序
- 交换排序
- 冒泡排序
- 快速排序
- 插入排序
- 直接插入排序
- 折半插入排序
- Shell排序
- 归并排序
- 桶式排序
- 基数排序
I. 冒泡排序
public class BubbleSortTest {
public static void main(String[] args) {
int[] arr = new int[10];
for(int i = 0; i < arr.length; i++) {
arr[i] = (int)(Math.random() * 100);
System.out.print(arr[i] + "\t");
}
//冒泡排序
for(int i = 0; i < arr.length - 1; i++) {
int count = 0;
for(int j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j + 1]){
arr[j] = arr[j] ^ arr[j + 1];
arr[j + 1] = arr[j] ^ arr[j + 1];
arr[j] = arr[j] ^ arr[j + 1];
count++;
}
}
if(count++ == 0){
break;
}
}
System.out.println("\n排序后的数组为:");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}
II. 快速排序
快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止所有内排算法中速度最快的一种。它是冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为 O ( n log n ) O(n\log{n}) O(nlogn)。
快速排序通常明显比同为
O
(
n
log
n
)
O(n\log{n})
O(nlogn)的其他算法更快,因此经常被采用。
快排采用了分治法的思想,所以再很多笔试面试中能经常看到快排的影子。
排序思想
- 从数列中挑出一个元素,称为“基准”(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的书可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个成为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 递归的最底部情形,是数列的大小是0或1,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
代码示例
/**
* 随机生成基准值的基本快速排序
*/
import java.util.Scanner;
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
//单数组排序
Scanner scan = new Scanner(System.in);
System.out.print("请输入随机数组下限(整数):");
int ll = scan.nextInt();
System.out.print("请输入随机数组上限(整数):");
int ul = scan.nextInt();
System.out.print("请输入随机数组长度(整数):");
int n = scan.nextInt();
int[] arr = new int[n];
for(int i = 0; i < arr.length; i++) {
arr[i] = (int)(Math.random() * (ul - ll + 1) + ll);
}
System.out.println("排序前的随机数组为:\n" + Arrays.toString(arr));
QuickSort(arr);
System.out.println("排序后的随机数组为:\n" + Arrays.toString(arr));
//排序测试
}
public static void QuickSort(int[] arr) {
//调用QuickSort(arr, low, high)
QuickSort(arr, 0, arr.length - 1);
}
public static void QuickSort(int[] arr, int low, int high) {
//先分区,后递归排序
if (low < high) {
int p = partition(arr, low, high);
QuickSort(arr, low, p - 1);
QuickSort(arr, p + 1, high);
}
}
public static int partition(int[] arr, int low, int high) {
//分区并返回基准值位置
//定义指针
int i = low;
int j = high;
//随机生成基准值
int p = (int)(Math.random() * (high - low + 1) + low);
//取中间值作为基本值
// int p = (int)((high + low) / 2);
//分区操作
while (i < j) {
//从左往右找到大于等于基准值的数字
while (i < j && arr[i] < arr[p]) {
i++;
}
//从右往左找到小于等于基准值的数字
while (i < j && arr[p] < arr[j]) {
j--;
}
if(i < j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
if(i == p) {
p = j;
i++;
}else if(j == p) {
p = i;
j--;
}else {
i++;
j--;
}
}
}
return p;
}
}
3-4 Arrays工具类的使用
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。
no. | expression | description |
---|---|---|
1 | boolean equals(int[] a, int[] b) | 判断两个数组是否相等 |
2 | String toString(int[] a) | 输出数组信息 |
3 | void fill(int[] a, int val) | 将指定值填充到数组之中 |
4 | void sort(int[] a) | 对数组进行排序 |
5 | int binarySearch(int[] a, int key) | 对排序后的数组进行二分法检索指定的值 |
3-5 数组使用中的常见异常
- 数组角标越界的异常: ArrayIndexOutOfBoundsException
- 空指针异常:NullPointerException
04 面向对象编程
面向对象:Object Oriented Programming
面向过程:Procedure Oriented Programming
前言:面向对象的思想概述
类(Class)和对象(Object)是面向对象的核心概念。
- 类是对一类事物的描述,是抽象的、概念上的定义
- 对象是实际存在的该类事物的每个个体,因而也成为实例(Instance)
4-1 Java类及类的成员
面向对象程序设计的重点是类的设计。设计类,就是设计类的成员。
A. 属性
- 属性 = 成员变量 = field = 域、字段
属性(成员变量) vs 局部变量
- 相同点
- 定义变量的格式相同:数据类型 变量名 = 变量值
- 先声明,后使用
- 变量都有其对应的作用域
- 不同点
- 在类中声明的位置不同
- 属性:直接定义在类的一对{}内
- 局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
- 关于权限修饰符的不同
- 常用的权限修饰符:private\public\缺省\protected
- 属性:可以在声明属性时,使用权限修饰符指明其权限
- 局部变量:不可以使用权限修饰符
- 默认初始化值的情况:
- 属性:类的属性,根据其类型,都有默认初始化值
- 整型(byte/short/int/long): 0
- 浮点型(float/double): 0.0
- 字符型(char): 0 (’\u0000’)
- 布尔型(boolean): false
- 引用数据类型(类、数组、接口): null
- 局部变量:没有默认初始化值。
- 在使用局部变量之前,必须显性赋值。
- 特别地,形参在调用时赋值即可
- 属性:类的属性,根据其类型,都有默认初始化值
- 内存中加载的位置不同
- 属性:加载到堆空间
- 局部变量:加载到栈空间
- 在类中声明的位置不同
B. 方法
- 方法 = 成员方法 = method
定义:方法描述了类应该具有的功能。
public void eat(){} //没有返回值,没有形参
public void sleep(int hour){} //没有返回值,带形参
public String getName(){} //返回值为字符串类型,不带形参
public String getNation(String nation){} //返回值为字符串类型,带形参
a. 方法的声明
权限修饰符 返回值类型 方法名(形参列表{方法体}
说明:
- 权限修饰符:
- Java规定的四种权限修饰符: private/public/缺省/protected -->详见封装性
- 返回值类型:有返回值 vs 没有返回值
- 如果方法有返回值,则必须在方法声明时指定返回值的类型;同时,方法中需要使用return关键字来返回值或常量。
- 方法名:属于标识符,遵循标识符的规则和规范,见名知意
- 形参列表:方法可以声明0个、1个或多个形参
- 格式:
- 数据类型1 形参1, 数据类型2 形参2
- 格式:
- 方法体:方法功能的体现
- return关键字的使用
- 适用范围:方法体中
- 作用
- 结束方法
- 针对有返回值类型的方法,返回数据
- 方法的使用中,可以调用当前类的属性或方法
b. 方法的重载(overload)
- 定义:在同一个类中,允许存在一个以上的同名方法,只要它们参数个数或者参数类型不同即可。
- 判断是否重载:
跟方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系 - 在通过对象调用方法时,如何确定某一个指定方法?
方法名 --> 参数列表
c. 可变形参的方法
JavaSE 5.0中提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。
从而可以用一种更简单的方法来传递个数可变的形参。
- 格式:数据名…变量名
- 传入参数个数可以是0或n
- 可变个数形参的方法与本类中方法名相同,形参类型也相同的数组方法之间不构成重载,不能通过编译
d. 方法参数的值传递机制
关于变量的赋值
- 如果变量是基本数据类型,此时赋值的是变量所保存的数据值。
- 如果变量是引用数据类型,此时赋值的是变量所保存的地址值。
方法的形参的传递机制:值传递
- 形参和实参的定义:
- 形参:方法定义时,声明的小括号内的参数
- 实参:方法调用时,实际传递给形参的数据
- 值传递机制
- 如果参数是基本数据类型,此时实参赋给形参的是实参实际真实存储的数据值
- 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值
e. 递归方法
定义:一个方法体内调用它自身。
- 方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复无需循环控制。
- 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环。
f. 方法的重写(override/overwrite)
定义:在子类中可以根据需要对父类中继承来的方法进行改造,也成为方法的重置、覆盖。
声明:
权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型 {
}
要求:
- 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
- 子类不能重写父类中声明为private权限的方法
- 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
- 父类被重写的返回值类型是void,则子类重写的方法的返回值类型只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或者A类的子类
- 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型
- 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
- 子类方法抛出的异常不能大于父类被重写方法的异常
注意:子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)
因为static方法是属于类的,子类无法覆盖父类的方法。
权限修饰符protected
不同package下的子类可以调用其父类protected的结构,不能调用private/(缺省)的结构。
C. 构造器(constructor)
构造器的作用
- 创建对象
- 可以初始化对象的属性
定义构造器的格式
权限修饰符 类名(形参列表) { }
构造器使用细节说明:
- 如果没有显式地定义类的构造器的话,则系统默认提供一个空参的构造器。
- 一个类中定义的多个构造器,彼此构成重载(OverLoad)
- 一旦显式地定义了类的构造器之后,系统就不再提供默认的空参构造器
- 一个类中,至少会有一个构造器
总结:属性赋值的先后顺序
- 默认初始化值
- 显式初始化
- 构造器中赋值
- 通过“对象.方法”或“对象.属性”的方式赋值
以上操作的先后顺序:1 -> 2
D. 拓展:JavaBean
JavaBean是一种Java语言写成的可重用组件。
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共构造器
- 有属性,且有对应的get、set方法
用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,
并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。
用户可以认为JavaBean提供了一种随时随地的复制和粘贴功能,而不用关心任何改变。
E. 代码块
- 代码块的作用:用来初始化类、对象
- 修饰符:只能用static修饰
- 静态代码块
- 内部可以有输出语句
- 随着类的加载执行,而且只执行一次
- 作用:初始化类的信息
- 执行顺序:如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行(由父及子,静态先行)
- 注意:静态代码块内只能调用静态的属性、方法,不能调用非静态的结构
- 非静态代码块
- 内部可以有输出语句
- 创建实例时执行,每创建一个对象执行一次非静态代码块
- 作用:可以在创建对象时对其属性进行初始化
- 执行顺序:如果一个类中定义了多个非静态代码块,则在创建对象时按照声明的先后顺序执行
- 非静态代码块内可以调用静态的属性、方法,也可以调用非静态的属性、方法
- 静态代码块
- 总结:对属性可以赋值的位置
- 默认初始化
- 显式初始化/在代码块中赋值
- 构造器中初始化
- 有了对象以后,可以通过"对象.属性"或调用相关方法进行赋值
F. 内部类
内部类的分类:成员内部类(静态、非静态) VS 局部内部类(方法内、代码块内、构造器内)
成员内部类
- 一方面,作为外部类的成员:
- 调用外部类的结构
- 可以被static修饰
- 可以被4中不同的权限修饰符修饰
- 另一方面,作为一个类:
- 可以定义属性、方法、构造器等
- 可以被final修饰,表示此类不能被继承
- 可以被abstract修饰
class Outer {
public String name;
public int count;
public void show() {
System.out.println("Class: Outer");
}
//成员内部类(静态)
static class Dog {
String name;
int age;
public void bark() {
System.out.println("喵喵喵");
// show();静态结构先加载,不能调用非静态方法
}
}
//成员内部类(非静态)
class Bird {
String name;
public Bird() {
}
{
show();
Outer.this.show();
}
public void sing() {
System.out.println("我是一只小小鸟");
}
public void display(String name) {
System.out.println(name);//方法的形参
System.out.println(this.name);//内部类的属性(ps就近原则)
System.out.println(Outer.this.name);//外部类的属性
}
}
}
局部内部类
开发中常见的内部类:
public class InnerClassTest {
//开发中少见的形式
public void method() {
//局部内部类
class AA {
}
}
//返回一个实现了Comparable接口的类的对象
public Comparable getComparable() {
//创建一个实现了Comparable接口的类:局部内部类
//方式1:非匿名形式
class MyCompare implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
return new MyCompare();
//
//方式2:匿名形式的实现类
// return new Comparable() {
// @Override
// public int compareTo(Object o) {
// return 0;
// }
// };
}
}
内部类的使用
public class InnerClassTest {
public static void main(String[] args) {
//创建Dog实例(静态的成员内部类):
Outer.Dog dog = new Outer.Dog();
dog.bark();
//创建Bird实例(非静态的成员内部类):
Outer o = new Outer();
Outer.Bird bird = o.new Bird();
bird.sing();
Outer.Bird bird2 = o.new Bird();
bird2.sing();
}
}
4-2 面向对象的三大特征
A. 封装性(Encapsulation)
程序设计思想:“高内聚、低耦合”
- 高内聚:类的内部数据操作细节自己完成,不允许外部干涉
- 低耦合:仅对外暴露少量的方法用于使用
I. 问题的引入:
当我们创建一个类的对象以后,我们可以通过“对象.属性”的方式,对其属性进行赋值。
这里赋值操作要受到属性的数据类型和存储范围的制约,除此之外,没有其他制约条件。
但是,在实际问题中,我们往往需要给属性赋值加入额外的限制条件(如private),使其只能通过方法进行有限制的访问。
–>此时,针对属性就体现了封装性。
II. 封装性的体现:
我们将类的属性私有化(private),同时,提供public方法来获取(getXxx)和设置(setXxx)此属性的值。
拓展:封装性的体现举例
- 如上
- 不对外暴露的私有的方法
- 单例模式
…
III. 权限修饰符
封装性的体现,需要权限修饰符来配合
- Java规定的4种权限(从小到大排列):private\缺省\protected\public
修饰符 | 类内部 | 同一个包 | 不同包的子类 | 同一个工程 |
---|---|---|---|---|
private | Yes | |||
(缺省) | Yes | Yes | ||
protected | Yes | Yes | Yes | |
public | Yes | Yes | Yes | Yes |
- 4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类
- 修饰类只能使用(缺省)和public;修饰类的内部结构则都可以
总结:Java提供了4种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用是的可见性的大小。
B. 继承性(Inheritance)
Why Inheritance?
- 减少了代码的冗余,提高了代码的复用性
- 便于功能的扩展
- 为之后多态性的使用提供了前提
格式
基本格式:class A extends B{}
- A:子类、派生类、subclass
- B: 父类、超类、基类、superclass
体现:一旦子类A继承父类B之后,子类A中就获取了父类B中声明的属性和方法
- 注意:私有的属性也能继承到,只不过不能直接调用属性(跟父类情况相同)
- 子类在继承父类之后,还可以声明自己特有的属性或方法,实现功能的拓展
- 子类和父类的关系不同于集合中的子集关系,子类相对于父类来说功能更丰富
Java中关于继承性的规定
Java只支持单继承和多层继承,不允许多重继承
- 一个子类只能有一个父类(C++可以有多重继承)
- 一个父类可以派生出多个子类
- class SubDemo extends Demo{} //ok
- class SubDemo extends Demo1,Demo2…{} //error
- 子类直接继承的父类成为直接父类,间接继承的父类称为间接父类
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法
I. Object类
Object是所有Java类的根父类。如果我们没有显式地声明一个类地父类的话,则此类继承于java.lang.Object类。
所有的Java类都直接或间接地继承于java.lang.Object类,都继承了它的结构和功能。
Constructor(s)
Object()
Methods
Modifier | Type | Method | Description |
---|---|---|---|
protected | Object | clone() | 复制一个对象 |
boolean | equals(Object obj) | ||
protected | void | finalize() | 当栈空间没有变量指向堆空间的对象时,将调用此方法回收堆空间中的对象 |
Class<?> | getClass() | 返回直接父类 | |
int | hashCode() | Returns a hash code value for the object | |
String | toString() |
- 面试题:==与equals的区别
- 回顾"=="运算符的使用:
- 可以使用于基本数据类型变量和引用数据类型变量中
- 如果比较的是基本数据类型,则实际比较的是两个变量存储的数据是否相等,不一定需要类型相同(‘A’ == 65)
- 如果比较的是引用类型变量,则实际比较的是两个变量指向的地址值是否相同
- equals()方法的使用:
- 需要通过对象来调用
- Object类中equals()的定义:与==作用相同,即比较对象的地址值
public boolean equals(Object obj) { return (this == obj); }
- String、Date、File、包装类等类中重写了equals(),比较对象的实际内容是否相同
- 自定义类重写equals()
- 原则:比较两个对象的实体内容是否相同(对象属性)
- 对称性:x.equals(y) == y.equals(x)
- 自反性:x.equals(x) == true
- 传递性
- 一致性:不管equals多少次,返回值不变
- 任何情况下,x.equals(null) == false
- 步骤1:地址相同(使用==)直接true
- 步骤2:长度不同直接false
- 步骤3:逐个比较属性,注意引用类型(如String)的属性要使用equals()
- 原则:比较两个对象的实体内容是否相同(对象属性)
- 回顾"=="运算符的使用:
class GeometricObject {
protected String color;
protected double weight;
protected GeometricObject() {}
protected GeometricObject(String color, double weight) {
this.color = color;
this.weight = weight;
}
public double getWeight() {
return weight;
}
public String getColor() {
return color;
}
public void setWeight(double weight) {
this.weight = weight;
}
public void setColor(String color) {
this.color = color;
}
public double findArea(){
return 0.00;
}
//自定义equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GeometricObject that = (GeometricObject) o;
return Double.compare(that.weight, weight) == 0 &&
Objects.equals(color, that.color);
}
@Override
public int hashCode() {
return Objects.hash(color, weight);
}
}
- toString()
- 当我们输出一个对象引用时,实际上就是调用当前对象的toString()
- Object类中toString()的定义:
public String toString() {return getClass().getName() + “@” + Integer.toHexString(hashCode())}
- String、Date、File、包装类等类中重写了toString()方法
- 自定义类重写toString()
- 原则:调用此方法时,返回对象的实体内容
II. 包装类(Wrapper)
包装类是Java针对8种基本数据类型定义的相应的引用类型。它使基本数据类型有了类的特点,
因而可以直接调用类中的方法,这时Java才是真正地面向对象。
基本数据类型 | Wrapper | 父类 |
---|---|---|
byte | Byte | Number |
short | Short | Number |
int | Integer | Number |
long | Long | Number |
float | Float | Number |
double | Double | Number |
boolean | Boolean | |
char | Character |
总结:基本类型、包装类与String类之间的相互转换
- 基本数据类型–>包装类:调用包装类的构造器
- 包装类–>基本数据类型:调用包装类的xxxValue()
- JDK 5.0加入了自动装箱与自动拆箱的新特性,不需要调用构造器和xxxValue()
- 基本类型、包装类 --> String类型:调用String类的valueOf()方法
- String类型 --> 基本数据类型、包装类:调用包装类的parseXxx()方法
III. 子类对象实例化过程
- 从结果上来看,子类继承父类以后,就获取了父类中声明的属性或方法;
创建子类的对象,在堆空间中,就会加载父类中声明的属性。 - 从过程上来看,当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器,
进而调用父类的父类的构造器,直到调用了java.lang.Object类中的空参构造器为止。
正因为加载过所有的父类的结构,所以才可以看到内存中父类的结构。
需要明确的是,虽然创建子类对象时调用了父类的构造器,但至始至终就创建了一个对象,即为new的子类对象。
C. 多态性(Polymorphism)
- 多态性可以简单理解为一个事物的多种形态。
- 何为多态性:
- 对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
- 多态性的使用细节:虚拟方法调用
- 虚拟方法调用:
- 子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,
动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
- 子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,
- 编译看左,运行看右
- 有了对象多态性之后,我们在编译期只能调用父类中声明的方法
- 当调用子父类同名、同参数的方法时,实际执行的是子类重写父类的方法
- 使用前提:
- 类的继承关系
- 子类重写父类的方法
- 注意:
- 对象的多态性只适用于方法,而不适用于属性(属性的编译和运行都看左边)
- 不能调用子类所特有的方法、属性;但内存中实际加载的是子类的属性和方法,由于声明为父类类型,导致编译时只能调用
父类中声明的属性和方法,子类特有的属性和方法不能调用。 - 如何调用子类特有的属性和方法?[向下转型:使用强制类型转换]
- 使用强转时,可能出现ClassCastException的异常
- 为了避免向下转型异常,先进行instanceof判断
- a instanceof A - 判断对象a是否时类A的实例,返回true/false
- 虚拟方法调用:
//向下转型使用细节的举例
public class PolymorphismTest {
public static void main(String[] args){
//问题1: 编译通过,运行时不通过
//举例1:
Person p1 = new Woman();
Man m1 = (Man)p1;
//举例2:
Person p2 = new Person();
Man m2 = (Man)p2;
//问题2:编译通过,运行也通过
Object obj = new Woman();
Person p3 = (Person)obj;
//问题3:编译不通过
//类型不匹配
Man m3 = new Woman();
}
}
class Person {}
class Man extends Person {}
class Woman extends Person {}
面试题:谈谈对多态的理解
- 实现代码的通用性
- Object类中定义的public boolean equals(Object obj){}
JDBC: 使用java程序操作(获取数据库连接、CRUD)数据库(MySQL\Oracle\DB2\SQL Server) - 抽象类、接口的使用体现了多态性。(抽象类、接口不能实例化)
小结:方法的重载和重写
从编译和运行的角度看:
- 重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。
对于编译器而言,这些同名方法就成了不同的方法。对于重载而言,在调用方法之前,编译器就已经确定了所要调用的方法,
这称为“早绑定”或“动态绑定”。- 他们的调用地址在编译期就绑定了
- Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法
- 对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法。这称为“晚绑定”或“静态绑定”。
4-3 其他关键字
this
this表示当前对象(或当前正在创造的对象,构造器中),可以调用类的属性、方法和构造器
a. this调用方法或属性
- 在方法内部使用,即这个方法所属对象的引用
- 在类的方法中,我们可以使用this.field或this.method的方式,调用当前对象属性或方法。但是通常情况下我们都选择省略。
- 特殊情况下,如果方法的形参和类的属性同名时,我们必须显式地使用this.field的方式,表明此变量是属性而非形参
- 在构造器内部使用,表示该构造器正在初始化对象
- 在类的构造器中,可以显式地使用“this(形参列表)”方式,调用本类中指定地其他构造器
- 构造器中不能调用自己(这个跟递归有本质的区别)
- 如果一个类中有n个构造器,则最多在n-1个构造器中进行其他构造器的调用
- 构造器调用必须放在首行,且最多调用一个其他构造器
b. this调用构造器
public class Person {
private String name;
//空参构造器
public Person() {
//Person初始化的40行代码
}
//带参数的构造器
public Person(String name) {
this();//执行空参构造器中的40行代码
this.name = name;
}
}
super
如this可理解为“自己的”一样,super可理解为“父类的”。
super可用来调用父类的属性、方法、构造器
a. 调用方法或属性
我们可以在子类的方法或构造器中,通过使用super.属性或super.方法,显式地调用父类中声明地属性或方法,但是通常情况下我们习惯省略super。
- 特殊情况1:当子类和父类中定义了同名的属性时,我们想要在子类中调用父类的属性,则必须显式调用。
- 特殊情况2:当子类重写了父类中的方法后,我们调用父类中被重写的方法时,则必须显式调用。
b. 调用构造器
我们可以在子类的构造器中显式地使用super(形参列表)的方式,调用父类中声明的指定的构造器。
- super(形参列表)必须声明在子类构造器的首行
- 我们在类的构造器中,针对this(形参列表)或super(形参列表)只能二选一
- 当我们在类的构造器中,没有显式地声明this(形参列表)或super(形参列表),则默认调用父类中空参的构造器。
- 在类的多个构造器中,至少有一个类的构造器使用了super(形参列表),调用父类的构造器
static
当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会
分配内存空间给对象,其方法才可以供外部调用。
我们有时候希望无论是否产生了对象或者无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。
static可以用来修饰:属性、方法、代码块、内部类(不能修饰构造器)
-
static修饰属性: 静态变量
- 静态属性(类变量) vs 非静态属性(实例变量)
- 非静态属性(实例变量):每个对象独立拥有一套类中的非静态变量
- 静态变量:类中实例共同拥有一个静态变量,当从一个实例中调用并修改该变量后,将影响到所有的实例
- static修饰属性的其他说明
- 静态变量随着类的加载而加载。可以通过"类.静态变量"的方式进行调用
- 静态变量的加载要早于对象的创建
- 由于类只会加载一次,则静态变量在内存中也只会存在一份(方法区的静态域中)
- 静态属性举例:
- System.out
- Math.PI
- 静态属性(类变量) vs 非静态属性(实例变量)
-
static修饰方法
- 随着类的加载而加载,可以通过"类.静态方法"的方式进行调用
- 在静态方法中,只能调用静态的方法或属性;非静态方法中,即可以调用静态的也可以调用非静态的方法或属性
-
static注意点:
- 在静态方法内,不能使用this关键字、super关键字(从生命周期去理解)
Q: 实际开发中,如何确定一个结构要不要声明为static?(类变量 or 实例变量?)
- 属性可以被多个对象共享
- 静态方法 or 非静态方法?
- 操作静态属性的方法通常设置为静态方法
- 工具类中的方法,习惯上声明为static
final
final: 可以用来修饰类、方法、变量
- final修饰一个类:此类不能被其他类所继承
- String类
- System类
- StringBuffer类
- final用来修饰方法:表明此方法不可以被重写
- Object.getClass();
- final用来修饰变量:此时的“变量”就是一个常量
- final修饰属性:
- 赋值位置:显式初始化\代码块中\构造器中
- final修饰局部变量:
- 使用final修饰形参时,表明此形参是一个常量。当调用此方法给常量形参赋一个实参,一旦赋值,就只能在
方法体内使用该形参,但不能重新赋值。
- 使用final修饰形参时,表明此形参是一个常量。当调用此方法给常量形参赋一个实参,一旦赋值,就只能在
- final修饰属性:
- static final用来修饰属性:称为全局常量
abstract
a. 抽象类与抽象方法
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般、更通用。类的设计应该保证父类和子类能够共享特征。
有时,将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
b. abstract关键字的使用
abstract可以用来修饰的结构:类、方法
- abstract修饰类:抽象类
- 此类不能实例化
- 抽象类中一定有构造器,便于子类实例化时调用
- 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作
- abstract修饰方法:
- 只有方法的声明,没有方法体
- 包含抽象方法的类一定是一个抽象类;反之,抽象类中可以没有抽象方法
- 只有当子类重写了父类所有的抽象方法后,此子类方可实例化;否则此子类只能声明为抽象类
- 使用注意事项:
- abstract不能用来修饰属性、构造器等结构
- abstract不能用来修饰private方法、static方法、final方法
c. 关于抽象类的匿名子类的使用
public class PersonTest {
public static void main(String[] args) {
method(new Student());//匿名对象
Worker worker = new Worker();
method1(worker);//非匿名的类,非匿名的对象
method1(new Worker());//非匿名的类的匿名对象
//创建了一个匿名子类的对象:p
Person p = new Person() {
public void breathe() {
System.out.println("吃东西");
};
public void speak(String lang) {
System.out.println("好好呼吸");
};
};
method1(p);
//创建匿名子类的匿名对象
method1(new Person() {
@Override
public void speak(String lang) {
System.out.println("匿名子类的匿名对象在说话");
}
@Override
public void breathe() {
System.out.println("匿名子类的匿名对象在呼吸");
}
});
}
public static void method1(Person p) {
p.breathe();
p.speak("Human Language");
}
public static void method(Student s) {
}
}
interface
在编程过程中,有两个方面的需求需要用接口来实现。
一方面,Java不支持多重继承,但有时必须从几个类中派生出一个子类,继承它们所有的属性和方法;
另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间没有is-a的关系,仅仅是具有相同的行为特征而已。
例如:鼠标、键盘、打印机、手机、数码相机等都支持USB连接。
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。
继承是一个“是不是”的关系,而接口实现则是“能不能”的关系。
接口的具体使用,体现了多态性。
a. 接口的使用
接口使用interface关键字来定义:
class AA extends BB implements CC,DD,EE {}
如何定义接口:定义接口中的成员
- JDK7以前:只能定义全局常量和抽象方法
* 全局常量:public static final,但书写时可以省略不写
* 抽象方法:public abstract - JDK8:除了全局常量和抽象方法之外,还可以定义静态方法、默认方法(略)
接口使用的注意事项
- Java中,接口和类是并列的两个结构
- 接口中不能定义构造器,这意味着接口是不能实例化的
接口的实现
Java开发中,接口通过类去**实现(Implements)**的方式来使用
* 如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化;否则只能作为一个抽象类
* Java类可以实现多个接口 --> 弥补了Java单继承性的局限性
* 接口与接口之间可以继承,且可以多继承
b. Java8中关于接口的改进
Java8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个
抽象定义的理念。
- 静态方法:使用static关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中
使用静态方法,例如Collection/Collections或Path/Paths这样成对的接口和类。- 只能通过接口来调用
- 默认方法:使用default关键字修饰。可以通过实现类对象来调用。
- 通过实现类对象,可以调用接口中的默认方法
- 如果实现类重写了接口中的默认方法,调用时仍然调用重写的方法
- 如果子类(或实现类)继承父类和实现的接口中声明了同名同参数方法,那么子类调用方法时遵循“类优先原则”
- 如果一个类实现了多个接口,而这多个接口中包含同名同参数的默认方法,那么必须在实现类中重写该方法,否则将
编译报错–>接口冲突 - 调用接口的默认方法的办法:接口名.super.方法名
package
为了更好地实现项目中类的管理,提供了包的概念。
a. 声明
位置:源文件首行
结构:"."代表文件目录的结构
注意事项:
- 包,属于标识符,遵循标识符的命名规则和规范,见名知意。
- 同一个包下不能命名同名的接口或类
b. JDK中主要的包
- java.lang: 包含一些Java语言的核心类,如String\Math\Integer\System\Thread
- java.net: 包含执行与网络相关的操作的类和接口
- java.io: 包含能提供多种输入/输出功能的类
- java.util: 包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数
- java.text: 包含一些java格式化相关的类
- java.sql: 包含java进行JDBC数据库编程的相关类/接口
- java.awt: 包含构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)
import
我们可以在源文件中显式地使用import结构导入指定包下的类、接口。import声明在package和class之间。
- 使用XXX.*的方式可以导入XXX包下的所有结构;如果使用的类或接口是java.lang则可以省略。(*只代表类,不包含子包)
- 如果在源文件中使用了不同包下同名的类,则必须至少有一个类需要以全类名的方式使用。
- import static XXX 表示导入XXX下的静态结构
05 其他专题
5-1 JVM内存结构
编译完源程序之后,生成一个或多个字节码文件。
我们使用JVM中的类的加载器和解释器堆生成的字节码文件进行解释运行。这意味着需要将字节码文件对应的类加载到内存中,涉及到内存解析。
《JVM规范》
- 虚拟机栈:即为平时提到的栈结构。我们将局部变量存储在栈结构中。
- 堆:我们将new出来的结构(比如:数组、对象)加载在堆空间中;对象的属性(非static)加载在堆空间中。
- 方法区:类的加载信息、常量池、静态域
5-2 设计模式
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。
A. MVC设计模式
MVC是常用的设计模式之一,将整个程序分为三个层次:视图模型层、控制器层、数据模型层。
这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变得灵活而且清晰,同时也描述了程序各个对象间的通信方式,
降低了程序的耦合性。
模型层(Model)
作用:处理数据
- 数据对象封装 - model.bean/domain
- 数据库操作类 - model.dao
- 数据库 - model.db
视图层(view)
作用:显示数据
- 相关工具类 - view.utils
- 自定义view - view.ui
控制层(Controller)
作用:处理业务逻辑
- 应用界面相关 - controller.activity
- 存放fragment - controller.fragment
- 显示列表的适配器 - controller.adapter
- 服务相关 - controller.service
- 抽取的基类 - controller.base
B. 单例(Singleton)设计模式
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得
其对象实例的方法。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器访问权限设置为private,这样
就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。由于在类的外部一开始无法得到类的对象,只能
调用该类的某个静态方法以返回内部创建的对象。静态方法也只能访问类中的静态成员变量,所以,只想类内部产生的该对象的变量
也必须定义为静态的。
实现方式:
- 饿汉式:一开始就把对象造好
- 懒汉式:什么时候用,什么时候才造对象
饿汉式和懒汉式的区别:
- 饿汉式:
- cons:对象一开始即创建好了,加载时间过长
- pros:饿汉式是线程安全的
- 懒汉式:
- pros:延迟对象的创建
- cons:线程不安全 --> 到多线程内容时再修改
单例模式的优点:
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,
如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单丽对象,然后
永久驻留内存的方式来解决。
例: java.lang.Runtime
应用场景
- 网站的计数器:一般是单例模式实现,否则难以同步
- 应用程序的日志应用
- 数据库连接池:因为数据库连接是一种数据库资源
- 项目中,读取配置文件的类一般也只有一个对象,没有必要每次使用配置文件数据都生成一个对象去读取。
- Application也是单例的典型应用
- Windows的Task Manager就是很典型的单例模式
- Windows的Recycle Bin也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例
C. 模板方法设计模式(TemplateMethod): 多态的应用
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,
但子类总体上会保留抽象类的行为方式。
解决的问题:
- 当功能内部一部分是显示确定的,一部分实现是不确定的,这是可以把不确定的部分暴露出去,让子类去实现。
- 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了,凡是某些部分易变,
易变部分可以抽象出来,供不同子类实现,这就是一种模板模式。
D. 代理模式(Proxy): 接口的应用
代理模式是Java开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。
public class NetworkTest {
public static void main(String[] args) {
Server s = new Server();
ProxyServer ps = new ProxyServer(s);
ps.browse();
}
}
interface Network {
public void browse();
}
class Server implements Network {
@Override
public void browse() {
System.out.println("真实的服务器访问网络");
}
}
class ProxyServer implements Network {
private Network network;
public ProxyServer(Network network) {
this.network = network;
}
public void check() {
System.out.println("联网之前的检查工作");
}
@Override
public void browse() {
check();
network.browse();
}
}
应用场景
- 安全代理:屏蔽对真实角色的直接访问
- 远程代理:通过代理类处理远程方法调用(RMI)
- 延迟加载:先加载轻量级的代理对象,真真需要再加载真实对象
- 例如,在开发一个大文档查看软件时,大文档中有大的图片,有可能一个图片有100MB,因此在打开文件时,不可能将所有的
图片都显式出来;这时就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开
- 例如,在开发一个大文档查看软件时,大文档中有大的图片,有可能一个图片有100MB,因此在打开文件时,不可能将所有的
分类
- 静态代理(静态定义代理类)
- 动态代理(动态生成代理类)
- JDK自带的动态代理,需要反射等知识
E. 工厂设计模式
实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
分类
- 简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
- 抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品无能为力;支持增加产品族)
5-3 程序的调试和debug
常用的调试的方法:
- 打印结果,逐行调试:System.out.println()
- 设置断点,使用IDEA中的Debug工具进行调试
5-4 理解main方法
main()方法的使用说明:
- main()方法作为程序的入口
- main()方法也是一个普通的静态方法
- main()方法可以作为我们与控制台交互的方式。(之前:Scanner)
- IJ交互方式:打开Run --> Edit Configuration --> 在program arguments中输入相关参数
- 控制台交互:运行时直接在命令行后输入参数(如下图)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EbEAwquK-1610863846786)(2021-01-02-15-01-57.png)]
5-5 异常处理
a. 异常概述
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会
遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,
网络是否始终保持通畅等等。
Java程序在执行过程中所发生的异常事件可分为两类:
- Error:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等眼中情况。比如StackOverflow和OuOfMemory。
一般不编写针对性的代码进行处理 - Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
- 空指针访问
- 试图读取不存在的文件
- 网络连接中断
- 数组角标越界
b. 异常体系结构
- java.lang.Throwable类
- java.lang.Error: 一般不编写针对性的代码进行处理
- java.lang.Exception: 可以进行异常的处理
- 编译时异常(checked)
- IOException
- FileNotFoundException
- ClassNotFoundException
- IOException
- RuntimeException(unchecked)
- NullPointerException
- ArrayIndexOutOfBoundsException
- ClassCastException
- NumberFormatException
- InputMismatchException
- ArithmeticException
- 编译时异常(checked)
c. 异常相关知识点
异常的处理:抓抛模型
-
Step1:抛
- 程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。
一旦抛出对象以后,其后的代码就不再执行。 - 关于异常对象的产生:
- 系统自动生成的异常对象
- 手动生成一个异常对象,并抛出(throw)
- 程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。
-
Step2:抓(可以理解为异常的处理方式)
- try-catch-finally
- throws
方式一:try-catch-finally
class Test() {
public void method() {
try {
//可能出现异常的代码
} catch (异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}
finally{
//一定会执行的代码
}
}
}
注意事项:
- finally使用注意点
- finally是可选的
- finally中声明的是一定会被执行的代码。即使①catch中又出现异常了②try-catch中有return,finally一定会执行。
- 什么情况下会把代码写到finally当中?
- 释放资源:如数据库连接、输入输出流、网络编程Socket等资源,JVM垃圾回收机制不能自动回收,需要手动进行
资源的释放
- 释放资源:如数据库连接、输入输出流、网络编程Socket等资源,JVM垃圾回收机制不能自动回收,需要手动进行
- 使用try将可能出现异常的代码包装起来,在执行过程中一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,
去catch中进行匹配;一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理,并且不继续往下匹配异常; - catch中的异常类型如果没有字符类关系,则声明顺序可忽略;如果满足子父类关系,则要求子类一定声明在父类的上面。
- 常用的异常对象处理方式:
- String getMessage()
- void printStackTrace()
- try-catch结构可以嵌套
其他细节:
- 体会1:使用try-catch-finally处理编译时异常,使得程序在编译时就不再报错,但是运行时仍可能报错,
它只是将一个编译时可能出现的异常延迟到运行时出现。 - 体会2:开发中,由于运行时异常比较常见,通常不针对运行时异常编写try-catch-finally,而针对编译时异常
一定要考虑异常的处理。
方式二:throws + 异常类型
- "throws + 异常类型"写在方法的声明处,指明此方法执行时,可能会抛出的异常类型。一旦方法体执行时出现异常,
仍然会在异常代码处生成一个异常类的对象,此对象满足throws后的异常类型时,就会被抛出,后续的代码不再执行。 - throws只是将异常对象抛给调用者,而try-catch-finally才是真正地将异常给处理掉了。
- 开发中如何选择使用throws还是try-catch-finally?
- 如果父类中被重写的方法没有throws方式,则子类重写的方法也不能使用throws;这意味着如果子类重写的方法中有异常,
必须使用try-catch-finally的方式处理。 - 执行的方法A中先后调用了另外几个以递进方式执行的方法,建议对这几个递进方法使用throws方式进行处理;在执行方法A
中考虑使用try-catch-finally方式
- 如果父类中被重写的方法没有throws方式,则子类重写的方法也不能使用throws;这意味着如果子类重写的方法中有异常,
自定义异常类
- 继承于现有的异常结构:RuntimeException\Exception
- 提供全局常量:serialVersionUID
- 提供异常类的构造器
d. 总结:异常处理的5个关键字
关键字 | 捕获异常 | 抛出异常 | 声明异常 |
---|---|---|---|
try | 执行可能产生异常的代码 | x | x |
catch | 捕获异常 | x | x |
finally | 无论是否发生异常,代码总被执行 | x | x |
throw | x | 异常的生成阶段:手动抛出异常类对象 | x |
throws | x | x | 声明方法可能要抛出的异常类 |