目录
Java概述
Java的特点
- 面向对象:三大特性封装、继承、多态。
- 健壮性:提供了一个相对安全的内存管理和访问机制。
- 跨平台性:只需要在Java应用程序的操作系统上先安装一个Java虚拟机(JVM),由JVM负责Java程序的运行。
Java两种核心机制
- Java虚拟机(Java Virtual Machine):是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器。该机制屏蔽了底层运行平台的差别,实现了”一次编译,到处运行“。对于不同的平台,有不同的虚拟机。
- 垃圾收集机制(Garbage Collection):在Java程序运行过程中自动进行垃圾回收,程序员不能精确控制和干预,但这并不意味着Java程序就不会出现内存泄漏或内存溢出。
JDK与JRE
JDK(Java Development Kit Java开发工具包):是提供给Java开发人员使用的,其中包括了Java的开发工具(编译工具javac.exe、打包工具jar.exe等),也包括了JRE。
JDK=JRE+开发工具集
JRE(Java Runtime Environment Java运行环境):包括了Java虚拟机(JVM)和Java程序所需要的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JRE=JVM+Java SE标准类库
简单而言,使用JDK的开发工具完成Java程序,然后交给JRE运行。
包
包声明语句用于把Java类放到特定的包中。一个Java源文件中,最多有一个package语句,且必须位于正式代码的第一行,如果没有提供package语句,就表明Java类位于默认包中,默认包没有名字。
包的作用:
- 区分名字相同的类;
- 控制访问权限;
- 组织和划分Java中的各个类。
包的命名规范:
包的名字通常全用小写,且包含以下信息:
- 类的创建者和拥有者的信息
- 类所属的软件项目的信息
- 类在具体软件项目所处的位置
包的命名规范采用了网上URL命名规范的反转形式,如:https://netstore.abc.com,Java包名的形式为:com.abc.netstore。
注释
单行注释: 格式 //注释内容
多行注释:格式 /*注释内容*/
创建JavaDoc:格式/**说明内容*/,用于类的声明、类的成员变量或者成员方法的声明。
API在线文档
第一个Java程序-HelloWorld
package basic;
public class HelloWorld {
public static void main(String args[]) {
System.out.println("Hello,world!");
}// Of main
}// Of class HelloWorld
总结:
Java程序的编写-编译-运行:
- 编写:编写的代码存储在以“.java”结尾的源文件中。
- 编译:可以使用javac.exe命令对源文件进行编译,获得字节码文件(".class"结尾的文件)格式:javac.exe 源文件.java。注:源文件中有几个类就会产生几个字节码文件。
- 运行:使用java.exe命令解释运行由编译后得到的字节码文件,格式:java 类名。
在一个Java源文件中,可以声明多个class,但是只能最多有一个类声明为public,且只能加在与源文件同名的类前面。
程序的入口是main()方法,格式有以下形式:
public static void main(String[] args)
public static void main(String args[])
static public void main(String[] args)
//由于static修饰的方法默认都是final类型(不能被子类覆盖),所以下面的格式也满足要求:
public final void main(String[] args)
输出语句:
- System.out.println() :输出完内容后换行。
- System.out.print():输出完内容后不换行。
Java基础语法
关键字和保留字
关键字:被Java语言赋予了特殊含义,用作专门用途的字符串(单词)。
保留字:现有Java版本尚未使用,但以后可能会作为关键字使用的字符串。
特点:关键字中所有字母都小写。
标识符
在Java中对变量、类、方法等要素命名时用到的字符序列称为标识符。
合法标识符规则
- 由26个英文字母大小写,0-9 ,_或 $ 组成
- 数字不可以开头。
- 不可以使用关键字和保留字,但能包含关键字和保留字。
- Java中严格区分大小写,长度无限制。
- 标识符不能包含空格。
Java中的名称命名规范:
- 包名:多单词组成时所有字母都小写:xxxyyyzzz
- 类名、接口名:多单词组成时,所有单词的首字母大写(大驼峰命名):XxxYyyZzz
- 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写(小驼峰命名):xxxYyyZzz
- 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ
注意1:“见名知意”。
注意2:java采用unicode字符集,因此标识符也可以使用汉字声明,但是不建议使用。
变量
概念:
- 内存中的一个存储区域
- 该区域的数据可以在同一类型范围内不断变化
- 变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值
作用:保存内存中的数据。
注意:
- 每个变量要先声明,后使用。
- 变量的作用域为:定义所在的一对{}内,只有在该变量的作用域内才有效。
- 使用变量名来访问这块区域的数据。
- 同一作用域不能重复声明变量。
声明变量:
语法:数据类型 变量名称;
例如: int var;
变量赋值:
语法:变量名称=值;
例如:var=100;
声明和赋值:
语法:数据类型 变量名 = 初始值;
例如: int var = 100;
变量的分类:
- 按照数据类型
- 基本数据类型:数值型(整数类型:byte,short,int,long、浮点类型:float,double)、字符型(char)、布尔类型(boolean)。
- 引用数据类型:类(class)、接口(interface)、数组([])。
- 按照声明位置不同
- 成员变量:在方法体外,类体内声明的变量,包括——实例变量(不以static修饰)、类变量(用static修饰)
- 局部变量:在方法体内部声明的变量,包括——形参(方法、构造器中定义的变量)、方法局部变量(在方法内定义)、代码块局部变量(在代码块内定义)
- 两者在初始化值方面的异同:
- 异:除了形参,其他变量都需要显式初始化。
- 同:都有生命周期。
整数类型:
类型 | 占用存储空间 | 表示范围 |
---|---|---|
byte | 1字节=8bit | -128 ~ 127 |
short | 2字节 | -2^15 ~ 2^15 - 1 |
int | 4字节 | -2^31 ~ 2^31 - 1(约21亿) |
long | 8字节 | -2^63 ~ 2^63 - 1 |
注意:Java的整型常量默认为int型,如果要声明long型常量,要在常量后加'l'或'L'。
例如:long var = 1234L;
浮点类型:
类型 | 占用存储空间 | 表示范围 |
---|---|---|
单精度float | 4字节 | -3.403E38 ~ 3.403E38 |
双精度double | 8字节 | -1.798E308 ~ 1.798E308 |
注意:
- Java的浮点型常量默认为double型,如果要声明float型常量,要在常量后加'f或'F'。
- float:尾数可以精确到7位有效数字,但很多情况下,这个精度很难满足需求。
- double:精度是float的两倍,通常采用此类型。
由上面的表格得:3.4*10^38>2*8^38
而且:2*8^38=2*2^114=2^115
可得:2^115 > 2^64-1
问:float占用存储空间和int的存储空间一样大,为什么float表示的数值范围比long还大?
float在内存中的存储:按照IEEE-754格式标准,将一个浮点数分为底数和指数两部分存储,其中最高位是符号位,后8位是指数位(表示范围:-126~128,由255-127得到),最后23位为底数部分。
1位符号位 | 8位指数位 | 23位尾数位 |
还记得科学记数法吗?小数点前只保留一位不是0的数字,其余部分都放在小数点后面,例如:19971400000000=1.99714×10^13。
在存储浮点数时也会采用同样的形式,只保留最高位不是0的数,其余部分都放在小数点后面,由于计算机采用的是二进制,所以最高位一定是1,索性就不再存储最高位,所以用23位表示的也是小数点后面的数值部分。
例如:float var=10.25f,在计算机中的存储过程为 (10.25)d=(110.01)b=(1.1001*2^2)b,所以指数为2,但要加上127,所以指数位表示的值应为129,既:(10000001)b,底数部分为:(1001)b,所以(10.25)f在内存中的存储结果为:01000000110010000000000000000000。
字符类型
char型数据用来表示通常意义上的“字符”(两个字节),在Java中的所有字符都使用Unicode编码。
字符型变量的三种表现形式:
- 用单引号包裹的单个字符,例如:char c1='a';char c2='国';
- 使用转义字符\将字符转变为特殊字符型常量,例如:char c3='\n';//表示换行符
- 直接使用Unicode值来表示字符型常量:'\uXXXX'(XXXX表示一个十六进制数),例如:\u000a表示\n。
- char类型可以通过它对应的Unicode码进行运算。
常见的转义字符
转义字符 | 说明 |
---|---|
\b | 退格符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\" | 双引号 |
\' | 单引号 |
\\ | 反斜线 |
布尔类型
boolean类型用作逻辑判断条件,一般用于流程控制,只允许取值true和false,没有null。与C语言不同的是,Java中不能使用0或非0的整数来替代false和true。
JVM中没有提供boolean值专用的字节码指令,在编译之后使用JVM中的int数据类型来代替:true用1表示,false用0表示。——《java虚拟机规范8版》
字符串类型
String不是基本数据类型,属于引用数据类型,使用方式与基本数据类型一致。一个字符串可以拼接另一个字符串,也可以直接串接其他类型的数据。
基本数据类型转换
自动类型转换:容量小的类型自动转换为容量大的数据类型。其表现为:在多种类型的数据混合运算时,系统会先自动将所有数据换成表达式中容量最大的那种数据类型,然后再进行计算。
- byte,short,char之间不会相互转换,他们三者在计算时首先转换为int类型。
- boolean类型不能与其他数据类型运算。
- 当任何基本数据类型的值和字符串(String)进行连接运算时(+),基本数据类型的值将自动转化为字符串类型。
强制类型转换
将容量大的数据类型转换为容量小的数据类型,需要加上强制转换符:(),但这可能造成精度降低或溢出,例如:long l = 123;int i = (int) (l);
通常,字符串不能直接转换为基本类型,但通过基本数据类型对应的包装类可以实现把字符串转换成基本类型,例如:String a ="12"; int i = Integer.parseInt(a);
boolean类型不可以转换为其他数据类型。
运算符
1:算术运算符:
注:
- 如果对负数取模,可以把模数负号忽略不计,如:7%-2=1。但被模数是负数时不可忽略负号,如:-7%2=-1。
- 整数除和小数除有区别,整数之间做除法时,只保留整数部分,舍弃小数部分。
- “+”除字符串相加功能外,还能把非字符串转换成字符串。如:System.out.println("5+5=" + 5 + 5);//输出结果为:5+5=55
2:赋值运算符:
符号“=”:
- 当“=”两侧数据类型不一致时,可以进行类型转换进行处理。
- 支持连续赋值
拓展赋值运算符:+=,-=,*=,/=,%=
在赋值运算过程中,要特别注意数据类型是否一致:
package basic;
public class Test {
public static void main(String[] arg) {
short s = 10;
// 常量1被默认为int类型,小容量的数据类型转换成大容量的数据类型需要强制转换
// s = s + 1; //Type mismatch: cannot convert from int to short
// +=不会改变数据类型
// s += 1; // 11
s++; // 11
System.out.println(s);
}
}
package basic;
public class Test {
public static void main(String[] arg) {
int i = 1;
i *= 0.1;
// 1*0.1=0.1,但i是int型数据,所以只会保留整数部分0
System.out.println(i);// 0
// 由上面的i=0进行运算,得到i=1
i++;
System.out.println(i);// 1
}
}
package basic;
public class Test {
public static void main(String[] arg) {
int m = 2;
int n = 3;
// 自增(后):先运算,再自增
n *= m++; // n*=m(m=2),m++
System.out.println("n=" + n); // n=6
System.out.println("m=" + m); // m=3;
}
}
package basic;
public class Test {
public static void main(String[] arg) {
int n = 10;
// 自增(后):先运算再自增;自增(前):先自增再运算
// 运算顺序: ++n = 11 ---> n = 10 + 10 +11 = 31 ---> n++ = 32
n += (n++) + (++n); // 32
System.out.println(n);
}
}
3:比较运算符
运算结果都是boolean型(要么true,要么false)。
注意比较运算符“==”不能写成赋值运算符“=”。
4:逻辑运算符
短路逻辑运算符的作用:减少运算量。
例如:如果&&的左边为假,则不再判断右边的真假,直接返回false;如果||的左边为真,则不再判断右边真假,直接返回true。
5:位运算
注意:
- 没有<<<运算符
- 位运算是直接对整数的二进制进行的运算
6:三元运算符
格式:条件表达式 ? 表达式1 : 表达式2;
如果条件表达式为true,运算结果是表达式1; 如果条件表达式为false,运算结果是表达式2。
注意:表达式1和表达式2为同一种类型。
运算符的优先级
只有单目算符、三元运算符、赋值运算符是从左向右运算的。
程序流程控制
程序流程控制语句是用来控制程序中各个语句执行顺序。
三种基本流程结构:
- 顺序结构:程序从上到下逐行执行,中间没有任何判断和跳转。
- 分支结构:根据条件,选择性执行某段代码。
- 循环结构:根据循环条件,重复执行某段代码。
分支结构:if、if-else、if-else if-......-else,switch-case
if分支结构使用说明:
- 条件表达式必须是布尔表达式(关系表达式或逻辑表达式)、布尔变量
- 语句块只有一条执行语句时,一对{}可以省略,但建议保留
- if-else语句结构,根据需要可以嵌套使用
- 当多个条件是“互斥”关系时,条件判断语句及执行语句间顺序无所谓;当多个条件是“包含”关系时,“小上大下 / 子上父下”
switch-case的使用
switch(表达式){
case 常量1:
语句1;
// break;
case 常量2:
语句2;
// break;
… …
case 常量N:
语句N;
// break;
default:
语句;
// break;
}
switch语句有关规则:
- switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 (jdk 5.0),String (jdk 7.0);
- case子句中的值必须是常量,不能是变量名或不确定的表达式值;
- 同一个switch语句,所有case子句中的常量值互不相同;
- break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有break,程序会顺序执行到switch结尾
- 当没有匹配的case时,执行default
循环结构:for循环、while循环、do-while循环、foreach语句
for (①初始化部分; ②循环条件部分; ④迭代部分){
③循环体部分;
}
说明:
②循环条件部分为boolean类型表达式,当值为false时,退出循环
①初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔
④可以有多个变量更新,用逗号分隔
①初始化部分
while(②循环条件部分){
③循环体部分;
④迭代部分;
}
说明:
注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。
for循环和while循环可以相互转换
①初始化部分;
do{
③循环体部分
④迭代部分
}while(②循环条件部分);
说明:
do-while循环至少执行一次循环体。
注意:循环可以嵌套使用。
foreach语句:是在Java SE5中引入的,为了简化遍历数组和集合的程序代码,即不必创建int变量去访问项构成的序列进行计数,foreach会自动产生每一项。
语法格式:
for(元素类型 元素变量x:待遍历的集合或数组) {
引用了变量x的java语句
}
使用:
package basic;
import java.util.Random;
public class ForeachStatement {
public static void main(String args[]) {
// 生成一个float类型数组
Random rand = new Random(47);
float f[] = new float[10];
for (int i = 0; i < 10; i++)
f[i] = rand.nextFloat();
// 使用foreach语句遍历数组
for (float x : f)
System.out.println(x);
}
}
特殊关键字的使用:break、continue、return
break的使用
break语句用于终止某个语句块的执行。
break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块。
label1: { ……
label2: { ……
label3: { ……
break label2;
……
}
}
}
continue的使用
- continue只能使用在循环结构中
- continue语句用于跳过其所在循环语句块的一次执行,继续下一次循环
- continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环
return的使用
return的功能是结束一个方法。当一个方法执行到一个return语句时,这个方法将被结束。
特殊流程控制语句说明
- break只能用于switch语句和循环语句中。
- continue 只能用于循环语句中。
- 二者功能类似,但continue是终止本次循环,break是终止本层循环。
- break、continue之后不能有其他的语句,因为程序永远不会执行其后的语句。
- 很多语言都有goto语句,goto语句可以随意将控制转移到程序中的任意一条语句上,然后执行它。但使程序容易出错。Java中的break和continue是不同于goto的。
数组
数组(Array)是指一组相同类型数据并且按一定顺序排列的集合,数组中的每一个数据称为元素。元素可以是任意类型(基本类型或引用类型)。
创建数组步骤:
- 声明一个数组类型的引用变量,简称为数组变量(注意:声明时不能指定数组长度)
- 用new语句构造数组的实例。new语句为数组分配内存,并且为数组中的每个元素赋予默认值。
- 初始化,即为数组的每个元素设置合适的初始值。
注意:
- 数组本身是引用数据类型,而其元素可以是任意类型。
- 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续地址空间的首地址。
- 数组的长度一旦确定,就不能修改。
- 可以直接通过下标(索引)的方式指定位置访问元素。
一维数组
声明方式:type var[]; 或 type[] var;
例:int a[];String[] s;
初始化
动态初始化:数组声明且为数组元素分配空间与赋值操作分开进行。如:
int[] arr = new int[3];
arr[0] = 3;
arr[1] = 9;
arr[2] = 8;
静态初始化:在定义数组的同时就为数组元素分配空间并赋值。如:
int arr[] = new int[]{ 3, 9, 8};
或
int[] arr = {3,9,8};
元素的引用:
每个数组都有一个属性length指明它的长度,如arr.length获得arr的长度
引用方式:数组名[数组元素下标] (下标从0开始到length-1)
元素的默认初始值
多维数组
创建多维数组时,必须按照从低维度到高维度的顺序创建每一维数组。
如:int[][][] a=new int[2][][3];//编译出错,要先创建第二维数组,再创建第三维数组
Arrays工具类的使用
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比 如排序和搜索)的各种方法。
boolean equals(int[] a,int[] b) | 判断两个数组是否相等。 |
String toString(int[] a) | 输出数组信息。 |
void fill(int[] a,int val) | 将指定值填充到数组之中。 |
void sort(int[] a) | 对数组进行排序。 |
int binarySearch(int[] a,int key) | 对排序后的数组进行二分法检索指定的值。 |
面向对象
概述
面向过程(POP) 与 面向对象(OOP)
- 二者都是一种思想,面向对象是相对于面向过程而言的。面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
- 面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。
相关概念
类和对象:
类:具有相同特征的事物的抽象描述,是抽象的、概念上的定义。
对象:实际存在的该类事物的每个个体,是具体的,因此也称为实例(insrance)。
属性和行为:
属性:类事物中的状态信息,对应类中的成员变量。
行为:类事物要做什么操作,或者基于事物的状态能做什么,对应类中的成员方法。
定义一个类:
//定义类
public class Phone {
//属性
public String name;
public double price;
//方法
public void call() {
System.out.println("拨打电话");
}
public void sendMessage(String paraMess) {
System.out.println("发送信息" + paraMess);
}
}
创建类的对象,并通过对象访问或调用属性或方法。
import oop01.Phone;
public class Main {
public static void main(String[] args) {
//创建对象
Phone phone = new Phone();
//通过对象,调用其声明的属性或方法
phone.name="aaa";
phone.price=111;
System.out.println("name = "+phone.name+", price = "+phone.price);
phone.call();
phone.sendMessage("hhh");
System.out.println("Hello world!");
}
}
类的内存解析:
栈:存储方法内定义的变量。
堆:存储new 出来的结构(比如:数组实体、对象实体),也包括对象中的属性。
方法区:存放类的模板。比如:Phone类的模板。
举例:
类的成员——变量(属性)、方法和构造器;代码块、内部类
成员变量和局部变量
- 在方法体外,类体内声明的变量成为成员变量;
- 在方法体内部等位置声明的变量称为局部变量。
两者的不同点:
- 类中声明的位置不同:成员变量声明在类内,方法体外;局部变量声明在方法或构造器内部的变量。
- 在内存中分配的位置不同:成员变量随着对象的创建,存放在堆空间中;局部变量存储在栈空间中。
- 生命周期:成员变量随着对象的创建而创建,随着对象的消亡而消亡;局部变量随着方法对应的栈帧入栈,局部变量会在栈中分配;随着方法对应的栈帧出栈,局部变量消亡。
- 作用域:成员变量在整个类的内部都是有效的;局部变量仅限于声明此局部变量所在的方法(或构造器、代码块)内。
- 是否可以有权限修饰符进行修饰:成员属性是可以用权限修饰符进行修饰的,而局部变量不能使用。
- 是否有默认值:成员变量都有默认初始化值,意味着如果没有给属性进行显性赋值,调用时直接使用默认值;局部变量都没有默认初始化值,意味着如果没有给局部变量进行显性赋值,调用时会报错。
补充:权限修饰符(public、protected、缺省、private)用于表明修饰的结构可调用的范围的大小。
方法
Java里面的方法不能独立存在,必须定义在类里面。
方法声明的格式:
权限修饰符 [其他修饰符] 返回值类型 方法名(形参列表) [throws 异常类型]{ //方法头
//方法体
}
权限修饰符:public\ protected\ 缺省\ private;
返回值类型:描述当调用此方法时,是否返回结果:
---> 无返回值:void。
---> 有返回值:需要指明返回的数据的类型,可以时基本数据类型,也可以时引用数据类型;并在方法中使用return 返回数据。
形参列表相关:
形参时局部变量,在函数调用时赋值,可以声明多个。
格式:(形参类型1 形参1, 形参类型2 形参2, 形参类型3 形参3, ... )
方法调用内存解析:
注:叉号表示方法运行结束后弹出栈操作;字符串赋值操作实际上是将堆中的地址赋给变量,图中为了更直观展示,直接将字符串赋给了变量。
方法重载
概念:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可(参数列表不同,意味着参数个数或者参数类型不同)。
特点:与修饰符、返回值类型无关,值看参数列表,且参数列表必须不同。调用时,根据参数列表的不同来区别。
重载方法的调用:JVM通过方法的参数列表,调用匹配的方法——
- 先根据个数、类型匹配最合适的方法;
- 再找个数和类型可以兼容的,如果同时有多个方法兼容,则会报错;
举例:
public class Phone {
//属性
public String name;
public double price;
//方法
public void call() {
System.out.println("拨打电话");
}
public void call(String number) {
System.out.println("拨打电话"+number);
}
public String call(String number, Boolean isReturn) {
System.out.println("拨打电话"+number+"; isReturn " + isReturn);
return number;
}
}
import oop01.Phone;
/**
* ClassName: ${NAME}
* Package:
* Description:
* @Author Xiyan Zhong
* @Create 2023/8/17 下午8:33
* @Version 1.0
*/
public class Main {
public static void main(String[] args) {
//创建对象
Phone phone = new Phone();
//通过对象,调用其声明的属性或方法
phone.name="aaa";
phone.price=111;
System.out.println("name = "+phone.name+", price = "+phone.price);
phone.call();
phone.call("123");
phone.call("123",Boolean.TRUE);
}
}
可变个数形参的方法
场景:在调用方法时,可能出现方法的形参的类型时确定的,但时参数的个数不确定,此时我们可采用可变形参个数的方法。
格式:(参数类型 ... 参数名);
说明:
- 在调用时,实参的个数大于等于0;
- 可变个数形参的方法可与同一个类中,同名的方法构成重构;
- 在可变个数形参的方法中,可将参数名看作是一个参数数组,可通过数组的方式访问各个参数,因此,当定义一个相同类型的参数数组时,将会报错,例如call(int ... numbers)和call(int[] numberarry);
- 可变个数形参必须声明在形参列表最后;
- 可变个数形参在一个方法中最多出现1次;
举例:
public class Test {
public static void main(String[] args) {
//创建对象
Test test = new Test();
test.call(0,0,1);
test.call(0,0,1,0);
test.call(new int[]{0,0,1,0,1,1});
}
//方法
public void call(int ... numbers) {
for (int i = 0;i<numbers.length;i++){
System.out.println(numbers[i]);
}
System.out.println("拨打电话" );
}
// 方法重载,当调用时 call(number1,number2, number3)时,自动调用第2个方法
public void call(int number1,int number2, int number3) {
System.out.println("拨打电话" + "call function 2" );
}
}
运行结果:
拨打电话call function 2
0
0
1
0
拨打电话
0
0
1
0
1
1
拨打电话
方法的值传递机制
基础知识:
- 基础数据类型在进行值传递时,传递的是数据值本身(因为变量实际存储的是数据值);
- 引用数据类型在进行值传递时,传递的是地址值(因为变量实际存储的是引用类型数据的地址),表示被赋值的变量同时指向该数据的地址。
形参:在定义方法时,方法名后面括号中声明的变量称为形式参数,简称形参。
实参:在调用方法时,方法名后面括号中使用的值/变量/表达式称为实际参数,简称实参。
方法的值的传递机制与变量赋值机制相同,当形参是基础类型时,将实参保存的数据值赋给形参;当形参是引用数据类型的变量时,将实参保存的地址值赋给形参。
举例:
public class Test {
public static void main(String[] args) {
//创建对象
Test test = new Test();
int m = 6;
System.out.println("before"+m);
test.method1(m);
System.out.println("after"+m);
int[] n = {0,1};
System.out.println("before"+n[0]);
test.method2(n);
System.out.println("after"+n[0]);
}
//方法
public void method1(int m){
m++;
}
public void method2(int[] n){
n[0] = 666;
}
}
before6
after6
before0
after666
构造器
构造器的作用:
- 搭配new关键字,创建类的对象;
- 在创建对象的同时,可以给对象的相关属性赋值;
构造器的声明格式: 权限修饰符 类名(形参列表){}
创建类以后,如果没有显示声明构造器,系统会默认生成一个空参的构造器,其权限与类的权限相同;一旦显示声明了构造器,则系统不再提供默认的空参构造器。
public class AnimalTest {
public static void main(String[] args){
Animal animal = new Animal(3);
animal.name = "狗";
animal.legs = 4;
System.out.println("name = "+animal.name + ", legs = "+animal.legs + ", age = "+animal.age);
}
}
class Animal{
// 属性
String name;
int legs;
int age;
//构造器
Animal(int num){
age = num;
}
}
代码块(不是很重要)
代码块的作用:用来初始化类或对象信息(即初始化类或对象的成员变量)。
代码块的修饰:只能使用static进行修饰
代码块的分类:
- 静态代码块
- 随着类的加载而执行;
- 由于类的加载只会执行一次,意味着静态代码块只会执行一次;
- 作用:初始化类的信息
- 内部可以声明变量、调用属性或方法、编写输出语句等操作
- 静态代码块的执行要先于非静态代码块的执行;
- 如果声明有多个静态代码块,则按照声明的先后顺序执行
- 静态代码块只能调用静态的结构
- 非静态代码块
- 随着对象的创建而执行;
- 每创建当前类的一个实例,就会执行一次非静态代码块;
- 作用:初始化对象的信息
- 内部可以声明变量、调用属性或方法、编写输出语句等操作
- 如果声明有多个非静态代码块,则按照声明的先后顺序执行
- 静态代码块可调用静态的结构也可以调用非静态结构
public class CodeBlockTest {
public static void main(String[] args) {
Person p1 = new Person();
}
}
class Person{
String name;
int age;
public Person() {
}
{
System.out.println("非静态代码块");
}
static {
System.out.println("静态代码块");
}
}
执行结果:
静态代码块
非静态代码块
静态代码块的执行测试
public class CodeBlockTest {
public static void main(String[] args) {
System.out.println(Person.info);
// Person p1 = new Person();
}
}
class Person{
String name;
int age;
static String info="这是静态属性";
public Person() {
}
{
System.out.println("非静态代码块");
}
static {
System.out.println("静态代码块");
}
}
静态代码块
这是静态属性
内部类
将一个类A定义在另一个类B中,A就称为内部类,B就称为外部类。
场景:当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只3为A提供服务,不在其他地方单独使用,那么整个内部完整的结构B最好使用内部类。
总的来说,遵循高内聚、低耦合的面向对象的开发原则。
内部类的分类:
举例:Thread中的State类,表示线程的生命周期;HashMap中声明了Node类,表示封装的key和value。
对成员内部类的理解:
举例——创建内部类对象:
public class InnerClassTest {
public static void main(String[] args) {
// 创建静态内部类的对象
Person.Dog dog = new Person.Dog();
dog.eat();
// 创建非静态内部类的对象
Person person = new Person();
Person.Bird bird = person.new Bird();
bird.eat();
}
}
class Person{
static class Dog{
public void eat(){
System.out.println("dog eat");
}
}
class Bird{
public void eat(){
System.out.println("bird eat");
}
}
}
举例——内部类调用与外部类同名属性
public class InnerClassTest {
public static void main(String[] args) {
// 创建非静态内部类的对象
Person person = new Person();
Person.Bird bird = person.new Bird();
bird.eat();
bird.show();
}
}
class Person{
String name="Tom";
int age = 15;
class Bird{
String name="Bird";
public void eat(){
System.out.println("bird eat");
}
public void show(){
System.out.println("age = "+age);
System.out.println("name = "+ this.name);
System.out.println("name = "+ Person.this.name);
}
}
}
JavaBean的理解
JavaBean是一种Java语言写成的可重用组件,是指符合以下标准的Java类:
- 类是公共的;
- 有一个空参的公共构造器;
- 有属性,且有对应的get、set方法。
用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Serclet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的赋值和粘贴的功能,而不关心任何改变。
public class People {
String name;
int age;
People(){}
public void setName(String n){
name = n;
}
public String getName(){
return name;
}
public void setAge(int n){
age = n;
}
public int getAge(){
return age;
}
}
面向对象的三大特征——封装、继承和多态
封装性
封装:所谓封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,对其他的类或对象隐藏信息。
实现封装:通过Java提供的四种权限修饰符来控制可见范围。
类:只能使用public和缺省修饰;类的内部成员可以由以上4种修饰。
继承
举例:
好处
继承的格式:
class A{
//属性、方法
}
class B extends A{
}
其中A可以称为父类、superClass、超类、基类;B可以称为子类、subClass、派生类。
使用继承之后,子类拥有父类声明的所有属性和方法(针对一些私有属性或方法,也继承了这些结构但是不能直接调用,并没有破坏封装性,对于私有属性可以通过方法进行访问),示例:
public class StudentTest {
public static void main(String[] args){
Student st = new Student();
st.setName("xxx");
// 通过getAge()方法访问超类的私有属性age
System.out.println("name = "+st.name + ", age = "+st.getAge()+ ", school = "+st.school );
}
}
class People{
String name;
private int age =9;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setAge(int n){
age = n;
}
public int getAge(){
return age;
}
}
class Student extends People{
String school = "swpu";
}
当一个类没有显示说明继承的类时,则默认继承java.lang.Object。
获取对象的父类:对象.getClass().getSuperclass()
Java的类支持多层继承:
Java的类只支持单继承,不支持多继承:
方法重写
方法重写:子类对从父类继承过来的方法进行的覆盖、覆写操作。
例如:
public class StudentTest {
public static void main(String[] args){
Student st = new Student();
st.setName("xxx");
// 通过getAge()方法访问超类的私有属性age
System.out.println("name = "+st.name + ", age = "+st.getAge()+ ", school = "+st.school );
}
}
class People{
String name;
private int age =9;
public void setName(String name){
System.out.println("Student method");
this.name = name;
}
public String getName(){
return name;
}
public void setAge(int n){
age = n;
}
public int getAge(){
return age;
}
}
class Student extends People{
String school = "swpu";
public void setName(String name){
this.name = name;
System.out.println("Student method");
}
}
具体规则:
- 父类中被重写的方法与子类重写的方法 的方法名和形参列表必须相同;
- 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符;
- 子类不能重写父类中权限为private的方法(如果在子类中定义了同父类相同的方法(父类中的权限时private)则认为是在子类中重新声明了该方法,并不是对父类的进行重写);
- 返回值类型:
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值必须是void;
- 父类被重写的方法的返回值类型时基本数据类型,则子类重写的方法的返回值类型必须与被重写的方法的返回值类型相同;
- 父类被重写的方法的返回值类型时引用数据类型(比如类),则子类重写的方法的返回值类型可以与被重写的方法的返回值类型相同或是被重写的方法的返回值类型的子类。
- 子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型的子类相同。
多态
对象中的多态性:父类的引用指向子类的对象。
格式: 父类类型 变量名 = 子类对象;
其中父类类型:指子类继承的父类类型,或实现的接口类型
在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象。
举例:
Person p = new Student();
Object o = new Person(); // Obejct类型的变量o,指向Person类型的对象
o = new Student(); // Object类型的变量o,指向Student类型的对象
作用:
- 虚拟方法调用:在多态的场景下,调用方法时,编译时,认为方法是左边声明的父类的类型的方法(即被重写的方法),但执行时,实际执行的是子类重写父类的方法。(简称:编译看左边,运行看右边)
多态的使用前提:有类的继承;有方法的重写。
多态的适用性:仅适用于方法(方法在编译阶段不能确定其调用入口地址,在运行阶段才能确定),不适用于属性(属性是在编译时确定的),即调用与子类同名的属性时,是父类的属性。
多态的好处与弊端:
- 好处:
- 变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好。
- 弊端:
- 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
多态的使用举例:
public class AnimalTest {
public static void main(String[] args){
Cat cat = new Cat();
test(cat);
Dog dog = new Dog();
test(dog);
}
public static void test(Animal animal){
System.out.println("type----"+animal.type);
animal.eat();
}
}
class Animal{
String type="base type";
public void eat(){
System.out.println("吃东西~~~~~");
}
}
class Cat extends Animal{
String type="Cat";
public void eat(){
System.out.println("猫吃鱼~~~~~");
}
}
class Dog extends Animal{
String type="Dog";
public void eat(){
System.out.println("狗吃骨头~~~~~");
}
public void dogMethod() {
System.out.println("狗急跳墙~~~~~");
}
}
type----base type
猫吃鱼~~~~~
type----base type
狗吃骨头~~~~~
转型的应用:
向上转型(多态):当左边的变量的类别(父类)> 右边对象/变量的类型(子类),我们称为向上转型。此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法。
向下转型的使用场景,在使用多态后,若想继续使用子类的特有的方法或属性,则可以通过向下转型的方式实现。
举例:
Animal a1 = new Dog();
// a1.dogMethod() 不可调用
// 向下转型
Dog dog1 = (Dog) a1;
dog1.dogMethod();
System.out.println(dog1 == a1); // dog1 与 a1 指向同一个堆空间,他们只是类型不同
向下转型可能出现的问题:
类型转换异常(ClassCastException),即出现下面的情况:
Animal a1 = new Dog();
// a1.dogMethod() 不可调用
// 向下转型
Cat cat1= (Cat) a1;
cat1.dogMethod();// ClassCastException
为了避免出现类型转换异常,建议在向下转型之前,使用instanceof 进行判断。
格式:a instanceof A:用于判断a是否是A的实例。
如果a instanceof A返回true,则a instanceof superA 也会返回true,其中A 是superA的子类。
如:
if(a1 instanceof Dog){
Dog dog1 = (Dog) a1;
dog1.dogMethod();
System.out.println(dog1 == a1);
}else {
System.out.println("a1不是Dog的实例");
}
关键字:package、import的使用
package:称为包,用于知名该文件中定义的类、接口等结构所在的包。
语法格式:package 顶层包名.子包名;
举例:pack1\pack2\PackageTest.java
package pack1.pack2; //制定类PackageTest属于包pack1.pack2
public class PackageTest{
public void display(){
System.out.println("in method display()");
}
}
说明:
- 一个源文件只能有一个声明包的package语句;
- package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包;
- 包名属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意;
- 包通常使用所在公司玉米,ing的导致:com.atguigu.xxx。
- 包对应于文件系统的目录,package语句中用"."来指明包(目录)的层次,每.一次就表示一层文件目录;
- 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构。不同的包下可以定义同名的结构;
包的作用:
- 包可以包含类和子包,划分项目层次,便于管理;
- 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式;
- 解决类命名冲突的问题;
- 控制访问权限;
JDK中主要的包的介绍:
- java.lang ——包含一些Java语言的核心类,如String, Math, Integer, System和Thread,提供常用功能;
- java.net ——包含执行与网络相关的操作的类的接口;
- java.io ——包含能提供多种输入/输出功能的类;
- java.util ——包含一些使用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数;
- java.text ——包含了一些java格式化相关的类;
- java.sql ——包含了java进行JDBC数据库编程的相关类/接口;
- java.awt ——包含了构成抽象窗口工具集(abstact window toolkits) 的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
import:用于显示引入指定包下所需要的类。
语法格式: import 包名.类名;
举例:
import pack1.pack2.Test; //import pack1.pack2.*; 表示引入pack1.pack2包中的所有结构
注意事项:
this的使用
this的功能:
- 它在方法(实例方法或非static的)内部使用,表示调用该方法的对象;
- 它在构造器内部使用,表示该构造器正在初始化的对象;
- this可以调用的结构:成员变量、方法和构造器;
在实例方法或构造器中,在调用当前类的成员变量或成员方法可以在其前面添加this,可以增加程序的可读性,但是,一般情况下都习惯省略this。但是,当形参和成员变量同名时,需要添加this来区分成员变量和局部变量,如:
public class PeopleTest{
public static void main(String[] str){
People people = new People("xxx");
System.out.println(people.name + " "+ people.age);
people.setName("ZXY");
System.out.println(people.name + " "+ people.age);
}
}
class People {
String name;
int age;
People(){
age = 18;
}
public People(String name){
this(); // 调用上面的构造器
this.name = name;
}
public void setName(String name){
this.name = name;
}
}
this调用构造器:
- 格式: this(形参列表);
- 可以在类的构造器中,通过this调用当前类的其他构造器;
- 要求:this(形参列表) 必须声明在当前构造器的首行(所以只能在构造器中最多能调用一次其他构造器);
super的使用
super的作用:子类可以通过该关键字调用与父类同名的属性、方法、构造器。
使用格式:
属性:super.属性
方法:super.方法(形参列表)
构造器: super(形参列表)
规定:通过super使用父类的构造器时,必须声明在子类构造器的首行,同时,子类构造器中通过this访问该类的其他构造器时也必须声明在首行,所以thi(形参列表)和super(形参列表)只能二选一;若子类的首行既没有thi(形参列表)也无super(形参列表),子类默认调用super(),即父类的空参构造器。
代码举例:
public class StudentTest {
public static void main(String[] args){
Student st = new Student();
st.method();
}
}
class People{
String doing;
People(){
System.out.println("这是父类的构造器");
doing = "打篮球";
}
public void method(){
System.out.println("这是父类的方法");
}
}
class Student extends People{
String doing;
Student(){
// 调用父类的构造器
super();
System.out.println("这是子类的构造器");
doing = "打羽毛球";
}
public void method(){
System.out.println("输出父类的 doing "+ super.doing);
System.out.println("输出子类的 doing "+ doing);
super.method();
System.out.println("这是子类的方法");
}
}
这是父类的构造器
这是子类的构造器
输出父类的 doing 打篮球
输出子类的 doing 打羽毛球
这是父类的方法
这是子类的方法
Object类
类java.lang.Object是类层次结构的根类,即所有其他类的父类,每个类都使用Object作为超类。
Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用。
Object类中声明的结构(属性和方法)就具有通用性:
- Object类中没有声明属性;
- Object类提供了一个空参的构造器;
- 重点关注Object类中声明的方法
clone():被对象调用,创建一个和源对象相同类型的新对象。
public class AnimalTest {
public static void main(String[] args){
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:"+a1);
System.out.println("a1[name] = "+a1.getName());
a2.setName("毛毛");
System.out.println("clone之后的对象:"+a2);
System.out.println("a2[name] = "+a2.getName());
System.out.println("a1[name] = "+a1.getName());
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
}
}
class Animal implements Cloneable{
private String name;
public Animal(){
super();
}
public Animal(String name){
this.name = name;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
finalize()
当了GC(垃圾回收器)要回收此对象时,调用此方法。注意,在该方法的内部避免出现内部循环引用,这会导致此对象不能被回收。
public class FInalizeTest {
public static void main(String[] args) throws InterruptedException {
Animal animal = new Animal("huahua");
System.out.println(animal);
animal=null; //此时对象实体就是垃圾对象,等待被回收,但时间不确定
System.gc(); //强制性释放空间
Thread.sleep(1000);
}
}
class Animal{
private String name;
public Animal(){
super();
}
public Animal(String name){
this.name = name;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->"+this);
}
}
eqeuals()
Object类中equals()方法的定义:
public boolean equals(Object obj) {
return (this == obj);
}
- 任何引用数据类型都可以使用。
- 在没有重写Object类中的equals()方法的情况下,该方法可以比较两个对象的引用地址是否相同。(或比较两个对象是否指向了堆空间中的同一个对象实体);
- 对于像String、File、Date和包装类等,它们都重写了Object类中的equals()方法,用于比较两个对象的实体内容是否相等。
public class EqualsTest {
public static void main(String[] args){
User u1 = new User("huahua",12);
User u2 = new User("huahua",12);
System.out.println(u1.equals(u2)); // 若没有重写方法,则比较两个地址,返回false
}
}
class User{
String name;
int age;
User(String name,int age){
this.name = name;
this.age = age;
}
// 重写equals 手动实现
// @Override
// public boolean equals(Object obj) {
// if(this ==obj){
// return true;
// }
// if(obj instanceof User){
// User user = (User) obj;
// return this.age ==user.age && this.name.equals(user.name);
// }
// return false;
// }
// 重写equals IDEA自动实现 alt+insert
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
}
区分: ==和equals()
- ==运算符
- 使用范围:基本数据类型、引用数据类型
- 基本数据类型:判断数据值是否相等
- 引用数据类型:比较两个引用变量的地址值是否相等
- equals()
- 使用范围:只能用于引用数据;
- 注意:当想要判断对象值是否相等,对equals()方法进行重写时,如果该类还包括其他自定类型,也要对其进行equals()方法重写,否则当前类调用equal()判断值是否相等时,在判断内部类的对象是否相等的时候,仍然调用的是Object类中的equals()方法,判断的是两个内部类的对象的地址是否相等。
toString()
Object类中toString()的定义:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
平时我们在调用System.out.println()打印对象引用变量时,其实就调用了对象的toString()方法。
- 自定义类在没有重写Object类中的toString()方法的情况下,默认返回的是当前对象的地址值;
- 对于像String、File、Date和包装类等,它们都重写了Object类中的toString()方法,返回当前对象的实体内容。
public class toStringTest {
public static void main(String[] args) {
User user = new User("huahua", 12);
// System.out.println(user.toString()); //未重写的情况下 oop10.User@4517d9a3
// System.out.println(user); // oop10.User@4517d9a3
System.out.println(user.toString());
}
}
class User{
String name;
int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 手动实现重写toString
// @Override
// public String toString() {
// return "User{ name = "+this.name+", age = "+this.age+"}";
// }
// IDEA自动重写toString
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
static关键字的使用
如果想让一个成员变量被雷的所有实例共享,就用static修饰即可,称为类变量(或类属性);同时,若想要有些方法的调用者和当前类的对象无关,也可通过static进行修饰,这样的方法通常被称为类方法。
以上说的类变量、类方法,只需要使用static修饰即可,所以也称为静态变量、静态方法。
(理解:静态是相对动态而言的,当类每次创建一个实例就是一个动态过程,所以实例方法只有创建类的对象(实例)后才能调用,而静态的属性和方法是不需要类创建实例依旧能使用)。
静态属性的理解:
public class StaticTest {
public static void main(String[] args) {
Chinese c1 = new Chinese("花花");
Chinese c2 = new Chinese("萌兰");
System.out.println(c1+"国家:"+c1.country);
System.out.println(c2+"国家:"+c2.country);
c1.country = "China";
System.out.println(c1+"country:"+c1.country);
System.out.println(c2+"country:"+c2.country);
// 通过类名直接访问
System.out.println(Chinese.country);
}
}
class Chinese{
// 静态属性
static String country="中国";
// 实例属性
String name="";
public Chinese() {
}
public Chinese(String name) {
this.name = name;
}
}
静态属性与实例属性的对比:
- 个数
- 静态属性:在内存空间中只有一份,被类的多个对象共享;
- 实例属性:类的每一个实例都保存着各自的实例属性;
- 内存位置
- 静态属性:jdk6及以前:存放在方法区;jdk7及以后:存放在堆空间中;
- 实例属性:存放在堆空间的对象实例中;
- 加载时机
- 静态属性:随着类的加载而加载,由于类只会加载一次,所以静态变量也只有一份;
- 实例属性:随着对象的创建而加载。每个对象拥有一份实例属性
- 调用者
- 静态属性:可以被类使用,也可以使用对象调用;
- 实例属性:只能使用对象调用;
- 判断是否可以调用--->从声明周期的角度解释
- 消亡时机:
- 静态属性:随着类的卸载而消亡;
- 实例属性:随着对象的消亡而消亡。
可以根据静态属性来理解静态方法,需要注意的是静态方法的调用问题:
- 静态方法内可以调用静态方法,不可以调用非静态结构;
- 实例方法内可以既调用静态结构,也可调用非静态结构;
- (根据生命周期来理解)
使用静态属性的情况:
- 判断当前类的多个实例是否能共享成员变量,且此成员变量的值时相同的;
- 开发中,常将一些常量声明是静态的,比如,Math类中的PI
使用静态方法的情况:
- 方法内操作的变量都是静态变量;
- 开发中,常常将工具类的的方法声明为静态方法,比如:Arrays类、Math类;
单例(Singleton)设计模式
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
实现思路:
饿汉式实现:
public class SingletonTest {
public static void main(String[] args) {
Bank bank1 = Bank.getInstance();
Bank bank2 = Bank.getInstance();
System.out.println(bank1.equals(bank2));
}
}
class Bank{
// 1.类的构造器私有化
private Bank(){
}
//2.在类的内部创建当前类的实例
//4.定义static属性,用于在static方法中访问
private static Bank instance = new Bank();
//3.使用getXX()方法获得当前类的实例,必须声明为static的
public static Bank getInstance(){
return instance;
}
}
懒汉式实现:
public class SingletonTest2 {
public static void main(String[] args) {
GirlFriend g1 = GirlFriend.getInstance();
GirlFriend g2 = GirlFriend.getInstance();
System.out.println(g1.equals(g2));
}
}
class GirlFriend{
//1. 类的构造器私有化
private GirlFriend(){}
//2. 声明当前类的实例
//4. 定义static属性,用于在static方法中访问
private static GirlFriend instance = null;
//3. 通过getXXX()获得当前类的实例,如果未创建对象,则在方法内部进行穿件
public static GirlFriend getInstance(){
if(instance == null){
instance = new GirlFriend();
return instance;
}
return instance;
}
}
两种实现方式的区别:
- 饿汉式:“立即加载”,随着类的加载,当前的唯一实例就创建了;
- 懒汉式:“延迟加载”,在需使用的时候,进行加载
优缺点:
- 饿汉式:(优点)写法简单,由于内存中较早加载,使用更方便、更快。线程是安全的。(缺点)内存中占用时间较长。
- 懒汉式:(优点)在需要的时候进行创建,节省内存空间。(缺点)线程不安全
单例模式的应用场景:
main()方法的理解
(main()方法作为程序的入口)由于JVM需要调用类的main()方法,所以该方法的访问权限必须是public,又因为JVM在执行main()方法时不必创建对象,所以该方法必须是static的,该访问接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。
理解1:看做是一个普通的静态方法;
理解2:看做是程序的入口,格式是固定的。
与控制台交互(从键盘获取数据):
- 使用Scanner
- 使用main()的形参进行传值。
final关键字的使用
final可以用来修饰的结构:类、方法、变量
- final修饰类:表示此类不能被继承,如String, StringBuffer, StringBuilder类;
- final修饰类:表示此方法不能被重写,如Object类中的getClass()
- final修饰变量:既可以修饰成员变量,也可以修饰局部变量,此时的变量就变成了常量,意味着一旦赋值,就不可更改。
final修饰成员变量:
- 给final变量赋值的方式:显示赋值;代码块中赋值;构造器中赋值(以上方法都是在创建对象的过程中进行的,像是方法中赋值就会报错,因为此时对象已经创建完成了,意味着这些常量不能再进行修改)
final修饰局部变量:一旦赋值就不能修改。
- 方法内部声明的局部变量:在调用局部变量前,一定需要赋值;
- 方法的形参:在调用此方法时,给形参进行赋值
final与static搭配:
- 修饰成员变量时,此成员变量称为全局常量,如 Math.PI;
abstract关键字
在父类中,只有方法名,没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类。
abstract修饰类:
- 此类称为抽象类;
- 抽象类不能实例化;
- 抽象类中是包含构造器的,因为子类对象实例化时,需要直接或间接的调用;
- 抽象类中可以没有抽象方法,反之,抽象方法搜在的类一定是抽象类;
abstract修饰方法:
- 此方法为抽象方法;
- 抽象方法只有方法声明,没有方法体;
- 抽象方法其功能是确定的(通过方法的声明确定),只是没有具体实现;
- 子类必须重写父类的所有抽象方法之后,才能实例化对象
abstract不能与哪些关键字公用:
- 不能用abstract修饰私有方法、静态方法、final的方法、final的类;
- 私有方法不能重写;
- 避免静态方法使用类进行调用;
- final的方法不能被重写;
- final的类不能有子类;
接口(interface)
接口就是规范,定义的是一组规则,体现了显示世界中“如果你是/要……则必须能……”的思想。继承是一个“是不是”的观is-a关系,而接口实现则是“能不能”得has-a关系。
定义结构的关键字:interface
接口内部结构说明:
- 可以声明
- 属性:必须使用public static final修饰
- 方法:jdk8之前,声明抽象方法,修饰为public abstract;jdk8:声明静态方法、默认方法;jdk9:声明私有方法。
- 不可以声明
- 构造器、代码块
举例:
public class InterfaceTest {
public static void main(String[] args) {
// Flyable fy = new Flyable();
System.out.println(Flyable.MIN_SPEED);
System.out.println(Flyable.MAX_SPEED);
}
}
interface Flyable {
// 全局常量
public static final int MIN_SPEED = 0;
// 可以省略public static
int MAX_SPEED = 7900;
// 也可以省略 public abstract修饰
public abstract void fly();
}
接口和类的关系:实现关系
格式:
[修饰符] class 实现类 implements 接口1,接口2,接口3……{
// 重写结构中抽象方法[必须],如果是实现抽象类,可以不重写
// 重写接口中默认方法[可选]
}
[修饰符] class 实现类 extends 父类 implements 接口1,接口2,接口3……{
// 重写结构中抽象方法[必须],如果是实现抽象类,可以不重写
// 重写接口中默认方法[可选]
}
举例:
public class InterfaceTest {
public static void main(String[] args) {
Plane plane = new Plane();
plane.fly();
plane.attack();
}
}
interface Flyable {
// 全局常量
public static final int MIN_SPEED = 0;
// 可以省略public static
int MAX_SPEED = 7900;
// 也可以省略 public abstract修饰
public abstract void fly();
}
interface Attackable{
void attack();
}
class Plane implements Flyable,Attackable{
@Override
public void fly() {
System.out.println("飞机飞上天");
}
@Override
public void attack() {
System.out.println("还会attack");
}
}
以上说明了:
- 类可以实现多个接口;
- 类针对于接口的多实现,一定程度上弥补了继承的局限性;
- 类必须将实现的接口中的所有的抽象方法都重写(或实现),否则此类必须声明为抽象类;
接口与接口的关系:继承关系,且可以多继承。
格式:
interface AA{
void method1();
}
interface BB{
void method2();
}
interface CC extends AA,BB{
}
class DD implements CC{
@Override
public void method1() {
}
@Override
public void method2() {
}
}
接口的多态性: 接口名 变量名 = new 实现类的形式;
如:Flyable f = new Plane(); f.flay();
举例:
public class USBTest {
public static void main(String[] args) {
// 1.创建接口实现类的对象
Computer computer = new Computer();
Printer printer = new Printer();
computer.transferData(printer);
// 2.创建接口实现类的匿名对象
computer.transferData(new Camera());
// 3.创建接口匿名实现类的对象
USB usb1 = new USB(){
@Override
public void start() {
System.out.println("U盘开始工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
};
computer.transferData(usb1);
// 4.创建接口匿名实现类的匿名对象
computer.transferData(new USB() {
@Override
public void start() {
System.out.println("扫描仪开始工作");
}
@Override
public void stop() {
System.out.println("扫描仪结束工作");
}
});
}
}
class Computer{
public void transferData(USB usb) {
System.out.println("设备连接成功……");
usb.start();
System.out.println("数据传输的细节操作……");
usb.stop();
}
}
class Camera implements USB{
@Override
public void start() {
System.out.println("照相机开始工作");
}
@Override
public void stop() {
System.out.println("照相机结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开始工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}
interface USB{
// 声明常量
// USB 的长 宽
//方法
public abstract void start();
void stop();
}
设备连接成功……
打印机开始工作
数据传输的细节操作……
打印机结束工作
设备连接成功……
照相机开始工作
数据传输的细节操作……
照相机结束工作
设备连接成功……
U盘开始工作
数据传输的细节操作……
U盘结束工作
设备连接成功……
扫描仪开始工作
数据传输的细节操作……
扫描仪结束工作
抽象类和接口的对比:
JDK8 的接口新特性
interface CompareA {
// 属性:声明为public static final
// 方法:jdk8之前,只能声明抽象方法
// 方法:jdk8中:静态方法
public static void method1(){
System.out.println("CompareA method1");
}
// 方法:jdk8中:默认方法
public default void method2(){
System.out.println("CompareA method2");
}
public default void method3(){
System.out.println("CompareA method3");
}
public default void method4(){
System.out.println("CompareA method4");
}
}
public interface CompareB {
public default void method3(){
System.out.println("CompareB method3");
}
}
public class SubClass extends SuperClass implements CompareA,CompareB{
@Override
public void method2() {
System.out.println("SubClass 可以直接调用或重写所实现接口中的默认方法");
}
@Override
public void method3() {
System.out.println("必须重写两个接口中的同名方法");
}
@Override
public void method4() {
System.out.println("SubClass method4");
}
// 5.关于调用同名方法的补充:
public void method(){
method4(); //调用自己的method4
super.method4();//调用父类的method4
method3(); //调用自己的method3
CompareA.super.method3(); //调用CompareA中的method3
}
}
public class SubClassTest {
public static void main(String[] args) {
// 1.接口中声明的静态方法只能使用接口来调用,不能使用其实现类来调用
CompareA.method1();
// SubClass.method1(); // 报错
// 2.
SubClass s1 = new SubClass();
// s1.method2(); // 如果实现类没有重写方法,则执行父类方法
s1.method2(); // 如果实现类重写了方法,则执行类中重写的方法
// 3.
// 类实现了两个接口,而两个接口中定义了同名同参数的默认方法,则实现类在没有重写此两个接口的情况下会报错--->接口冲突
// 要求:此时实现类必须重写接口中定义的同名同参数的方法
s1.method3();
// 4.类优先原则:
// 子类(或实现类)继承了父类并实现了接口。父类和接口中声明了同名同参数的方法。
// (其中,接口中的方法是默认方法。)
// 默认情况下,子类(或实现类)在没有重写此方法的情况下,调用的是父类中的方法。
// 如果重写方法,则调用子类(或实现类)中重写后的方法。
s1.method4();
}
}
枚举类
枚举类型本质上也是一种类,只不过是这个类的对象时有限的、固定的几个,不能让用户随意创建。
若枚举只有一个对象,则可以作为一种单例模式的视线方法。
jdk5.0之前:
public class SeasonTest {
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
}
class Season{
// 声明实例属性
private final String name;
private final String description;
// 私有化类的构造器
private Season(String name,String description){
this.name = name;
this.description = description;
}
// 提供实例变量的get方法
public String getName() {
return name;
}
public String getDescription() {
return description;
}
// 创建实例
public static final Season SPRING = new Season("春天","万物复苏");
public static final Season SUMMER = new Season("春天","夏日炎炎");
public static final Season AUTUMN = new Season("春天","秋高气爽");
public static final Season WINTER = new Season("春天","白雪皑皑");
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
在jdk5.0以后:
使用enum关键字定义的枚举类,默认其父类是java.lang.Enum类;且使用其定义的枚举类,不要再显示定义父类,否则会报错。
Enum类中常用的方法:
举例:
public class SeasonTest1 {
public static void main(String[] args) {
Season1 spring = Season1.SPRING;
System.out.println(spring.getClass());
System.out.println(spring.getClass().getSuperclass());
// 测试常用方法
// 1. toString()
System.out.println(spring);
System.out.println("******************************");
// 2. name()
System.out.println(spring.name());
System.out.println("******************************");
// 3. values()
Season1[] values = Season1.values();
for (int i=0;i<values.length;i++){
System.out.println(values[i]);
}
System.out.println("******************************");
// 4. valueOf(String objName)
// 返回当前枚举类中当前名称为objName的枚举类对象;如果不存在该对象,则报错。
String objName = "WINTER";
Season1 season1 = Season1.valueOf(objName);
System.out.println(season1);
System.out.println("******************************");
// 5. ordinal()
// 返回实例对象在枚举类中被声明的次序(角标)
System.out.println(Season1.AUTUMN.ordinal());
}
}
// jdk5.0中使用enum关键字定义枚举类
enum Season1{
// 1. 必须在枚举类的开头声明多个对象,对象之间使用逗号隔开
SPRING("春天","万物复苏"),
SUMMER("春天","夏日炎炎"),
AUTUMN("春天","秋高气爽"),
WINTER ("春天","白雪皑皑");
// 2. 声明实例属性
private final String name;
private final String description;
// 3. 私有化类的构造器
private Season1(String name,String description){
this.name = name;
this.description = description;
}
// 4. 提供实例变量的get方法
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}
枚举类实现接口的操作
- 枚举类实现接口,在枚举类中重写接口中的抽象方法。当通过不同的枚举对象调用此方法时,执行的是同一个方法;
- 让枚举类的每一个对象重写接口中的抽象方法。当通过不同的枚举对象调用此方法时,执行的是不同的方法;
Annotation注解
注解(annotation)是从JDK5.0开始引入,以“@注解名”在代码中存在,例如:
- @Ovrride:限定重写父类方法,该注解只能用于方法。
- @Deprecated:用于表示所修饰的元素(类、方法)已过时。通常是因为所修饰的结构危险或存在更好的选择。
- @SuppressWarning(value = "unchecked"):抑制编译器警告。
生成文档相关的注释:
Annotation可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,还可以添加一些参数值,这些信息被保存在Annotation的“name=value”中。
注解可以在类编译、运行时进行加载,体现不同的功能。
注解与注释:注解也可以看做是一种注释,通过Annotation修饰,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入写补充信息。但是,注解和注释之间也存在差异:
- 对于单行和多行注释是给程序员看的;
- 而注解是可以被编译器或其他程序读取的,程序还可以根据注解的不同,做出相应的处理。
注解的重要性:
元注解:对现有的注解进行解释说明的注释。
包装类
Java提供了两个类型系统,基本数据类型与引用数据类型。使用基本类型在于效率,当使用只针对对象设计的API或新特性(例如泛型)时,则需要进行包装类。
Java针对巴中基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点后。就可以调用类中的方法,Java才是真正的面向对象。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Blooean |
char | Character |
其中,Byte、Short、Integer、Long、Float、Double的父类都是Number。
基本数据类型与包装类之间的转换:
- (装箱)基本数据类型 --> 包装类:
- 包装类 变量名 = new 包装类(值); // 该方式已被弃用;
- 包装类 变量名 = 包装类.valueOf(值);// 推荐方式
- (拆箱)包装类 --> 基本数据类型:调用 包装类.Value();
jdk5.0的自动装箱和自动拆箱新特性:
- 自动装箱:基本数据类型 变量1=值;包装类 变量2 = 变量1;
- 自动拆箱:包装类 变量1 = 值;基本数据类型 变量2 = 变量1;
JUnit单元测试
测试分类:
- 黑盒测试:不需要写代码,给输入值,观察程序是否能够输出期望的值;
- 白盒测试:需要写代码的。关注程序具体的执行情况。
JUnit是由Erich Gamma和Kent Beck编写的一个测试框架(regression testing framework),供Java开发人员编写单元测试用。
JUnit测试是程序员测试,即所谓百合测试,因为程序换知道被测试的软件如何完成功能和完成什么样的功能。
需要导入的包:junit-4.12.jar和hamcrest-core-1.3.jar
(等需要的时候再补充吧)
异常处理
异常:指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。
Java中把不同的异常用不同的类表示,一旦发生某种异常,就创建该类异常类型的对象,并抛出(throw)。然后我们可以捕获(catch)这个异常对象并处理;如果没有捕获这个异常对象,那么这个异常对象将会导致程序终止。
Java异常体系
Throwable
java.lang.Throwable 类是Java程序执行过程中发生的异常事件对应的类的根父类。
Throwable中常用的方法:
- public void printStackTrace():打印异常的详细信息。包含了异常的类型、异常的原因、异常出现的位置、在开发和调试阶段都得使用printStackTrace。
- public String getMessage():获取发生异常的原因
Error和Exception
Throwable可分为两类:Error和Exception。分别对应着java.lang.Error和java.lang.Exception两个类。
Error:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
- 例如StackOverflowError和OutOfMemoryError
Exception:编程错误或偶然的外在因素导致的一般性问题,需要使用针对的代码进行处理,使程序继续运行。一旦发生异常,程序也会挂掉。
- 空指针访问
- 试图读取不存在的文件
- 网络连接中断
- 数组角标越界
编译时异常和运行时异常
异常处理的方式
Java采用的异常处理机制是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简介、优雅,并且易于维护。
一:try-catch-finally
过程1:“抛”——程序在执行的过程中,一旦出现异常,就会在异常的代码处生成异常类的对象,并将此对象抛出,一旦抛出,此程序就不执行其后的代码了。
过程2:“抓”——针对于过程1中的异常对象,进行捕获处理。此捕获处理的过程,就成为抓。一旦将异常进行了处理,代码就继续执行。
基本结构:
使用细节:
- 将可能出现异常的代码声明在try中,一旦代码出现异常,就会自动生成一个对应异常类的对象,并将此对象抛出;
- 针对于try中抛出的异常类的对象,使用之后的catch语句进行匹配。一旦匹配上,就进入catch语句进行处理,一旦处理结束,代码就可以继续向下执行;
- 如果声明了多个catch结构,不同的异常类型在不存在父子关系的情况下,可以任意设置前后(上下)关系;如果多个异常类型满足父子类的关系,则必须将子类声明在父类结构上面(因为如果发生了异常,父类在上面的话,直接进入父类所在的catch进行处理异常,子类的catch就没有用了),否则,报错。
- catch中异常处理的方式:
- 自己编写输出的语句;
- 通过Throwable的printStackTrace和getMessage获取详情信息和获取发生异常的原因。
- try中声明的变量,出了try结构后,就不能调用了。
- try-catch结构可以嵌套使用。
开发体会:
- 对于运行时异常:开发中,通常不在进行显示处理,一旦程序执行时异常,那么久根据异常的提示信息修改代码。
- 对于编译时异常:一定要处理,否则编译不通过。
关于finally的使用:
- 将一定要被执行的代码声明在finally结构中——无论try或catch中是否存在仍未被处理的异常,无论try或catch中是否存在return语句等,finally中的声明语句都一定会执行(且如果try或catch中有return,那么finally中的语句会在它们的return之前执行,如果finally也有return,那么就不再执行try和catch中的return)。
- 唯一例外——使用System.exit(0)来终止当前正在运行的Java虚拟机。
- finally语句和catch语句是可选的,但finally语句不能单独使用;
- 需要放在finally的代码:
- 一些资源(如:输出/输出流、数据库连接、Socket连接等资源),在使用完以后,必须显示的进行关闭操作,否则,GC不会自动的回收这些资源,进而导致内存的泄露。为了保证这些资源在使用完以后,不管是否出现异常,都要保证这些资源都被关闭。
二:throws + 异常类型
格式:在方法的声明处使用"throws 异常类型1,异常类型2,……"
抛出异常:
处理异常:
- 从编译是否通过的角度看,可以把这种方式看成是给出了万一出现异常的处理方案。即,继续向上抛出(throws)。
- 但是,此throws的方式,仅是将可能出现的异常抛给了此方法的调用者,此调用者仍然需要考虑如何处理相关的异常。从这个角度上看,throws的方式不算是真正的处理异常。
方法重写的要求:子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被重写的方法抛出的异常类型的子类。
开发中,选择相应的异常处理方式的场景:
手动抛出异常对象
在实际开发中,如果出现不满足具体场景的代码问题,就有必要手动抛出一个异常。
关键字:throw
(抛出异常时传递给异常类的构造器的字符串就是异常信息,可以通过getMessage()获得。)
自动抛出异常和手动抛出异常:
- 自动:程序在执行过程中,一旦出现异常,就会在出现异常的代码处自动生成对应异常类的对象,并将此对象抛出;
- 手动:程序在执行过程中,不满足指定条件的情况下,主动使用“throw+异常类的对象”方式抛出异常对象。
自定义异常
定义:
- 继承与现有的异常体系。通常集成于RuntimeException \ Exception
- 通常提供几个重载的构造器
- 提供一个全局常量,声明为: Static final long serialVersionUID = xxxxxxL;
使用:
- 在具体的代码中,在满足指定条件的情况下,需要手动的使用“throw + 自定义异常的对象”方式,将异常对象抛出。
- 如果自定义异常类是非运行时异常类,则必须考虑如何处理此类异常类的对象。(具体的: try-catch-finally 或 throws)
自定义异常的作用:
在开发时,我们更关心的是,通过异常的名称就能直接判断此异常出现的原因,所以当不满足我们指定的条件时,指明我们自己特有的异常类,通过此异常类的名称,就能判断出具体出现的问题。
多线程
相关概念
线程调度:
线程的创建
概述:
方式一:继承Thread类
// 1. 创建Thread的子类
class PrintNumber extends Thread{
// 2. 重写run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 3. 创建当前Thread对象
PrintNumber t1 = new PrintNumber();
// 4. 通过对象调用start()--> 启动线程;调用当前线程的run()方法
t1.start();
// main()这个线程执行的操作,会和t1发生线程交互
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i+"*********************");
}
}
}
}
注意:
- 不能通过直接调用run()方法替换start(),这并未创建线程,仍然是在main的线程中。
- 不能让已经start的线程再次执行start操作,若想再创建一个新的线程,则需要new一个新的线程对象。
方式二:实现Runable接口
// 1.创建一个实现Runnable接口的类
class EvenNumberPrint implements Runnable{
// 2.重写run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
// 3.创建当前实现类的对象
EvenNumberPrint p =new EvenNumberPrint();
// 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(p);
// 5.Thread实例调用start()
t1.start();
/**
* 拓展:再创建一个线程,没用与便利100以内的偶数
*/
Thread t2 = new Thread(p);
t2.start();
}
}
两种方式对比:
- 共同点:
- 启动线程,使用的都是Thread类中定义的start();
- 创建的线程对象,都是Thread类或其子类的实例
- 不同点:一个是类的继承,一个是接口的实现
建议:使用通过接口的方式实现。
Runnable实现的好处:
- 通过实现接口的方式,避免了类的单继承的局限性;
- 更适合处理有共享数据的问题;
- 实现了代码和数据的分离。
联系:public class Thread implements Runnable (代理模式)
线程的常用方法和生命周期
线程中常用的构造器
线程中的常用方法:
线程优先级相关
线程的生命周期
JDK1.5之后:
线程的安全问题与线程的同步机制
发生安全问题的情况:
保证线程安全
方式一:同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明:
- 需要被同步的代码:即为操作共享数据的代码;
- 共享数据:即多个线程需要操作的数据;
- 需要被同步的代码,在被sychronized包裹以后,就使得这一线程在操作这些代码的过程中没其他线程必须等待;
- 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码;
- 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须使用同一个同步监视器。
注意:在实现Runable接口的方式中,同步监视器可以考虑使用this;在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用——当前类.class。
方式二:同步方法
- 如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法(用synchronized修饰)即可。
- 非静态的同步方法,默认同步监视器是this;静态的同步方法,默认同步监视器是当前类本身。
synchrionized:
- 好处:解决了线程的安全问题;
- 弊端:在操作时,多线程其实是串行执行的,意味着性能低。
线程安全的懒汉式
饿汉式:(类加载时就实例化,并且创建单例对象)不存在线程安全问题。
懒汉式:(默认不会创建实例化,用的时候再创建)存在线程安全问题。需要使用同步机制来处理,即,在创建的实例时加上synchrionized。
public class BankTest {
static Bank b1 = null;
static Bank b2 = null;
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
b1 = Bank.getInstance();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
b2 = Bank.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(b1);
System.out.println(b2);
System.out.println(b1 == b2);
}
}
class Bank {
private Bank() {
}
private static Bank instance = null;
// public static Bank getInstance() {
// if (instance == null) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// instance = new Bank();
//
// }
// return instance;
// }
// 方式一:
// public static synchronized Bank getInstance() {
// if (instance == null) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// instance = new Bank();
//
// }
// return instance;
// }
// 方式二:
// public static Bank getInstance() {
// synchronized (Bank.class){
// if (instance == null) {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// instance = new Bank();
//
// }
// }
// return instance;
// }
// 方式三: 效率比方式一和二更高
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
// 注意这里的判断不能省略
if (instance == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new Bank();
}
}
}
return instance;
}
}
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
通过Lock避免死锁:
class Window extends Thread{
static int ticket = 100;
// 1. 创建lock实例,需要确保多个线程共用同一个lock(必须有static final)
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
// 2.锁定对共享资源调用
lock.lock();
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " 售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
// 3. 释放共享资源
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
synchronized同步方式与Lock方式对比:
- synchronized不管是同步代码块还是同步方法,都需要再结束一对{}之后,释放对同步监视器的调用;
- Lock是通过两个方法控制需要被同步的代码,更灵活一些。
- Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
线程通信
当我们需要多个线程来共同完成一件任务,并且希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调他们的工作,以实现多线程共同操作一份数据。
注意:
- 此三个方法的使用,必须实在同步代码块或同步方法中;
- Lock需要配合Condition实现线程间的通信;
- 此三个方法的调用者,必须是同步监视器,否则,会报IllegalMonitorStateException异常;
- 此三个方法声明在Object中;
class PrintNumber implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。
// (如果被wait()的多个线程的优先级相同,则随机唤醒一个)
// 被唤醒的线程从当初被wait的外置继续执行。
this.notify();
if (number <= 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": " + number);
number++;
try {
this.wait(); // 线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
break;
}
}
}
}
}
public class PrintNumberTest {
public static void main(String[] args) {
PrintNumber p = new PrintNumber();
Thread t1 = new Thread(p, "线程1");
Thread t2 = new Thread(p, "线程2");
t1.start();
t2.start();
}
}
wait()和sleep()比较:
- 相同点:一旦执行,当前线程都会进入阻塞状态;
- 不同点:
- 声明的位置:wait()声明在Object中,sleep()声明在Thread中,静态方法。
- 使用场景:只能使用在同步代码块或同步方法中;sleep()可以在任何场景下使用。
- 使用在同步代码块或方法中的,wait()一旦执行,会释放同步监视器,sleep()一旦执行,不会释放同步监视器。
- 结束阻塞的方式:wait():到达指定时间自动结束阻塞 或通过被notify唤醒,结束阻塞;sleep():到达指定时间自动结束阻塞。
新增两种创建线程的方式
实现Callable(jdl5.0新增)
线程池
好处:
- 提高了程序执行的效率。(因为线程已经提前创建好了)
- 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
- 可以设置相关的参数,对线程池中的线程的使用进行管理。
缺陷:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
常用类与基础API
String的理解与使用
注意:若是两个相同的字符串常量,则它们在内存中的地址相同。
- 当对字符串重新赋值时,需要重新指定一个字符串常量的位置进行赋值,不能在原有的位置修改。
- 当对现有的字符串进行拼接操作时,需要重新开辟空间保存拼接以后得字符串,不能在原有的位置修改。
- 当调用字符串的replace()替换现有的某个字符时,需要重新开辟空间保存拼接以后得字符串,不能在原有的位置修改。
所以,通过new的方式创建一个字符串对象,实际在内存中创建了两个对象,一个是对空间中new的对象,另一个是字符串常量池中生成的对象。
时间相关API
JDK8之前的API:
JDK8之后的API:
Java比较器
java.lang.System
java.lang.Runtime
java.lang.Math
java.math
java.util.Random
集合框架
添加元素:
判断:
删除:
其他:
迭代器接口
执行原理:
List接口
特点:
遍历:
Set
Map
Collections工具类
泛型
泛型方法:
泛型之间的继承:
泛型通配符的使用(当泛型的类存在继承关系的时候使用,如上面的情况1):
IDEA快捷键
查看或设置快捷键:Settings-->Keymap
数据结构
List实现类源码分析
Map实现类源码分析
File类
Java 的I/O
更好的实现方式:把资源关闭操作放在了finally,即使在读取文件过程中发生异常时,也能保证资源的的关闭;同时使用了if 进行判断,只有打开资源后才执行资源关闭操作,提高了程序的健壮性。
非文本文件只能通过字节流进行读/写操作:
结合缓冲流的使用:
字符和字节之间的转换(转换流):
对象流
自定义类的序列化过程及要求:
可序列化的意义:网络中的数据传输;不同进程之间的通信。
其它流的使用:
Java学习资料
尚硅谷Java零基础全套视频教程