java

前言、入门程序、常量、变量


常用DOS命令

  • 常用命令

    命令 操作符号
    盘符切换命令 盘符名:
    查看当前文件夹 dir
    进入文件夹命令 cd 文件夹名
    退出文件夹命令 cd..
    退出到磁盘根目录 cd\
    清屏 cls

Java虚拟机——JVM

  • JVM(Java Virtual Machine ):Java虚拟机,简称JVM,是运行所有Java程序的假想计算机,是Java程序的 运行环境,是Java 最具吸引力的特性之一。我们编写的Java代码,都运行在 JVM 之上。

  • 跨平台:任何软件的运行,都必须要运行在操作系统之上,而我们用Java编写的软件可以运行在任何的操作系 统上,这个特性称为Java语言的跨平台特性。该特性是由JVM实现的,我们编写的程序运行在JVM上,而JVM 运行在操作系统上。

JRE和JDK

  • JRE ( Java Runtime Environment) :是Java程序的运行时环境,包含JVM和运行时所需要的核心类库

  • JDK ( Java Development Kit):是Java程序开发工具包,包含JRE和开发人员使用的工具

我们想要运行一个已有的Java程序,那么只需安装JRE即可
我们想要开发一个全新的Java程序,那么必须安装JDK

小提示:
三者关系: JDK > JRE > JVM

程序开发步骤说明

Java程序的三个步骤:编写编译运行

  • 编写
1
2
3
4
5
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
  • 编译

在DOS命令行中,进入java源文件的目录,使用javac命令进行编译。

命令:

1
javac java源文件名.后缀名

举例:

1
javac HelloWorld.java

编译成功后,命令行没有任何提示。打开 目录,发现产生了一个新的文件 HelloWorld.class ,该文件 就是编译后的文件,是Java的可运行文件,称为字节码文件,有了字节码文件,就可以运行程序了。

Java源文件的编译工具javac。exe,在JDK安装目录的bin目录下。但是由于配置了环境变量,可以在任意目录下使用。

  • 运行

在DOS命令行中,进入java源文件的目录,使用java命令进行运行。

命令:

1
java 关键字

举例:

1
java HelloWorld

Java HelloWorrld 不要写 不要写 不要写 .class

Java程序.class文件的运行工具java.exe,在JDK安装目录的bin目录下。但是由于配置了环境变量,可以在任意目录下使用。

入门程序说明

  1. 编译和运行是俩回事

    • 编译:是指将我们编写的java源文件翻译成JVM认识的class文件,在这个过程中,javac编译器会检查我们所写的程序是否有错误,有错误就会提示出来,如果没用错误就会编译成功。
    • 运行:是指将class文件交给JVM去运行,此时JVM就会去执行我们编写的程序了。
  2. 关于main方法

    • main方法:称为主方法。写法是固定格式不可以更改。main方法是程序的入口点或者起始点,无论我们编写多少程序,JVM在运行的时候,都会从main方法这里开始执行。
  3. 添加注释comment

    • 注释:就是对代码的解释和说明。其目的是让人们能够更加轻松的了解代码。为代码加注释,是十分重要的,他不影响程序的编译和运行。

    • Java中有实现注释(包括单行注释和多行注释)和文档注释

      • 单行注释以 //开头 换行结束

      • 多行注释以 /*开头 以*/结束

      • 文档注释以 /**开头 以*/结束

        1
        2
        3
        /**
        *对程序、类和变量的解释
        */
  4. 关键字keywords

    • 关键字:是指在程序中,Java已经定义好的单词,具有特殊含义。
      • HelloWorld案例中,出现的关键字有publicclassstaticvoid等,这些单词已经被Java定义好,全部是小写字母,notepad++中颜色特殊。
  • 关键字比较多,不能死记硬背,学到哪里记到哪里即可。

常用关键字

abstract else interface super char for private transient
boolean extends long switch class if protected try
break false native synchronized continue implements public true
byte final new this default import return void
case finally null throw do instanceof short Volatile
catch float package throws double int static While
  1. 标识符
    • 标识符:是指在程序中,我们自己定义内容。比如类的名字、方法的名字和变量的名字等等,都是标识符。
      • HelloWorld案例中,出现的标识符有类名字 HelloWorld
    • 命名规则: 硬性要求
      • 标识符可以包含英文字母26个(区分大小写)0-9数字$(美元符号)_(下划线)
      • 标识符不能以数字开头。
      • 标识符不能是关键字。
    • 命名规范:软性建议
      • 类名规范:首字母大写,后面每个单词首字母大写(大驼峰式)。
      • 方法名规范: 首字母小写,后面每个单词首字母大写(小驼峰式)。
      • 变量名规范:全部小写。

常量

常量:在程序运行期间,固定不变的量。

常量的分类

  1. 字符串常量:凡是用双引号引起来的部分,叫做字符串常量。例如:”abc“、”Hello“、”123
  2. 整数常量:直接写上的数字,没有小数点。例如:1002000-250
  3. 浮点数常量:直接写上的数字,有小数点。例如:2.5-3.140.0
  4. 字符常量:凡是用单引号引起来的单个字符,就做字符常量。例如:’A‘、’b‘、’9‘、’
  5. 布尔常量:只有量中取值。truefalse
  6. 空常量:null。代表没有任何数据。
类型 含义 数据举例
整数常量 所有的整数 0,1, 567, -9
小数常量 所有的小数 0.0, -0.1, 2.55
字符常量 单引号引起来,只能写一个字符,必须有内容 ‘a’ , ‘ ‘, ‘好’
字符串常量 双引号引起来,可以写多个字符,也可以不写 “A” ,”Hello” ,”你好” ,””
布尔常量 只有两个值(流程控制中讲解) true , false
空常量 只有一个值(引用数据类型中讲解) null

变量

  • 变量:程序运行期间,内容可以发生改变的量。

数学中,可以使用字母代替数字运算,例如 x=1+5 或者 6=x+5。

程序中,可以使用字母保存数字的方式进行运算,提高计算能力,可以解决更多的问题。比如x保存5,x也可 以保存6,这样x保存的数据是可以改变的,也就是我们所讲解的变量。

Java中要求一个变量每次只能保存一个数据,必须要明确保存的数据类型。

数据类型

数据类型分类

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

  • 基本数据类型:包括 整数浮点数字符布尔
  • 引用数据类型:包括 数组接口

基本数据类型

四类八种基本数据类型:

数据类型 关键字 占用内存 取值范围
字节型 byte 1个字节 -128~127
短整型 short 2个字节 -32768~32767
整形 int(默认) 4个字节 -2^31^~2^31^-1
长整型 long 8个字节 -2^63^~2^63^-1
单精度浮点数 float 4个字节 -3.4E38(3.4×10^38^)~3.4E38(3.4×10^38^)
双精度浮点数 double(默认) 8个字节 -1.7E308(1.7×10^308^)~1.7E308(1.7×10^308^)
字符型 char 2个字节 任意字符(0-65535)
布尔类型 boolean 1个字节 true,false

Java中的默认类型:整数类型是 int 、浮点类型是 double

变量的定义

变量定义的格式包括三个要素: 数据类型变量名数据值

格式

1
数据类型 变量名 = 数据值;

long类型:建议数据后加L表示。

float类型:建议数据后加F表示。

使用变量的时候,有一些注意事项:

  1. 如果创建多个变量,那么变量之间的名称不可以重复。
  2. 对于floatlong类型来说,字母后缀F和L不要丢掉。
  3. 如果使用byte或者short类型的变量,那么右侧的数据值不能超过左侧类型的范围。
  4. 没有进行赋值的变量,不能直接使用;一定要赋值之后,才能使用。
  5. 变量使用不能超过作用域的范围。
    【作用域】:从定义变量的一行开始,一直到直接所属的大括号结束为止。
  6. 可以通过一个语句来创建多个变量,但是一般情况不推荐这么写。

数据类型转换、运算符、方法入门

数据类型转换


Java程序中要求参与的计算的数据,必须要保证数据类型的一致性,如果数据类型不一致将发生类型的转换。

自动转换

  • 自动转换:将取值范围小的类型自动提升为取值范围大的类型
1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i = 1;
byte b = 2;
// byte x = b + i; // 报错
//int类型和byte类型运算,结果是int类型
int j = b + i;
System.out.println(j);
}

转换原理图解

byte 类型内存占有1个字节,在和 int 类型运算时会提升为int 类型 ,自动补充3个字节,因此计算后的结果还是 int 类 型。

同样道理,当一个 int 类型变量和一个 double 变量运算时, int 类型将会自动提升为 double 类型进行运算。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i = 1;
double d = 2.5;
//int类型和double类型运算,结果是double类型
//int类型会提升为double类型
double e = d+i;
System.out.println(e);
}

转换规则

范围小的类型向范围大的类型提升, byteshortchar 运算时直接提升为 int

1
byteshortchar‐‐>int‐‐>long‐‐>float‐‐>double

强制转换

1.5 赋值到 int 类型变量会发生什么?产生编译失败,肯定无法赋值。

1
int i = 1.5; // 错误

double 类型内存8个字节, int 类型内存4个字节。 1.5double 类型,取值范围大于 int 。可以理解为 double 是8 升的水壶, int 是4升的水壶,不能把大水壶中的水直接放进小水壶去。

想要赋值成功,只有通过强制类型转换,将 double 类型强制转换成 int 类型才能赋值。

  • 强制类型转换:将 取值范围大的类型强制转换成 取值范围小的类型

比较而言,自动转换是Java自动执行的,而强制转换需要我们自己手动执行。

转换格式:

1
数据类型 变量名 = (数据类型)被转数据值;

1.5 赋值到 int 类型,代码修改为:

1
2
// double类型数据强制转成int类型,直接去掉小数点。
int i = (int)1.5;

同样道理,当一个 short 类型与 1 相加,我们知道会类型提升,但是还想给结果赋值给short类型变量,就需要强制转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//short类型变量,内存中2个字节
short s = 1;
/*
出现编译失败
s和1做运算的时候,1是int类型,s会被提升为int类型
s+1后的结果是int类型,将结果在赋值会short类型时发生错误
short内存2个字节,int类型4个字节
必须将int强制转成short才能完成赋值
*/
s = s + 1//编译失败
s = (short)(s+1);//编译成功
}

转换原理图解

==强烈注意==

  • 浮点转成整数,直接取消小数点,可能造成数据损失精度。
  • int 强制转成 short 砍掉2个字节,可能造成数据丢失。
1
2
3
4
// 定义s为short范围内最大值
short s = 32767;
// 运算后,强制转换,砍掉2个字节后会出现不确定的结果
s = (short)(s + 10);

ASCII编码表

1
2
3
4
5
6
7
8
public static void main(String[] args) {
//字符类型变量
char c = 'a';
int i = 1;
//字符类型和int类型计算
System.out.println(c+i);//输出结果是98
}

在计算机的内部都是二进制的0、1数据,如何让计算机可以直接识别人类文字的问题呢?就产生出了编码表的概念。

  • 编码表:就是将人类的文字和一个十进制数进行对应起来组成一张表格。

    人们就规定:

    字符 数值
    0 48
    9 57
    A 65
    Z 90
    a 97
    z 122
    • 将所有的英文字母,数字,符号都和十进制进行了对应,因此产生了世界上第一张编码表ASCII( American Standard Code for Information Interchange 美国标准信息交换码)。

小贴士

在char类型和int类型计算的过程中,char类型的字符先查询编码表,得到97,再和1求和,结果为98。char类型提升 为了int类型。char类型内存2个字节,int类型内存4个字节。

运算符


算术运算符

算术运算符包括
+ 加法运算,字符串连接运算
- 减法运算
* 乘法运算
/ 除法运算
% 取模运算,两个数字相除取余数
++-- 自增自减运算

Java中,整数使用以上运算符,无论怎么计算,也不会得到小数。

1
2
3
4
public static void main(String[] args) {
int i = 1234;
System.out.println(i/1000*1000);//计算结果是1000
}
  • ++运算,变量自己增长1。反之, -- 运算,变量自己减少1,用法与 ++ 一致。

    • 独立运算:

      • 变量在独立运算时, 前++ 和后++ 没有区别 。
      • 变量 前++ :例如 ++i
      • 变量 后++ :例如 i++
    • 混合运算:

      • 和其他变量放在一起, 前++后++ 就产生了不同。
      • 变量 前++ :变量a自己加1,将加1后的结果赋值给b,也就是说a先计算。a和b的结果都是2。
      1
      2
      3
      4
      5
      6
      public static void main(String[] args) {
      int a = 1;
      int b = ++a;
      System.out.println(a);//计算结果是2
      System.out.println(b);//计算结果是2
      }
      • 变量 后++ :变量a先把自己的值1,赋值给变量b,此时变量b的值就是1,变量a自己再加1。a的结果是2,b 的结果是1。
      1
      2
      3
      4
      5
      6
      public static void main(String[] args) {
      int a = 1;
      int b = a++;
      System.out.println(a);//计算结果是2
      System.out.println(b);//计算结果是1
      }
  • + 符号在 字符串中的操作:

    • + 符号在字符串的时候,表示连接、拼接的含义。
    • “a”+”b”的结果是”ab”,连接含义。
1
2
3
public static void main(String[] args){
System.out.println("5+5="+5+5);//输出5+5=55
}

赋值运算符

比较运算符包括:
= 等于号
+= 加等于
-= 减等于
*= 乘等于
/= 除等于
%= 取模等
  • 赋值运算符,就是将符号右边的值,赋给左边的变量。
1
2
3
4
5
public static void main(String[] args){
int i = 5;
i+=5;//计算方式 i=i+5 变量i先加5,再赋值变量i
System.out.println(i); //输出结果是10
}
  • 注意事项:
    1. 只有变量才能使用赋值运算符,常量不能进行赋值。
    2. 复合赋值运算符其中隐含了一个强制类型转换。

比较运算符

比较运算符包括:
== 比较符号两边数据是否相等,相等结果是true。
< 比较符号左边的数据是否小于右边的数据,如果小于结果是true。
> 比较符号左边的数据是否大于右边的数据,如果大于结果是true。
<= 比较符号左边的数据是否小于或者等于右边的数据,如果小于结果是true。
>= 比较符号左边的数据是否大于或者等于右边的数据,如果小于结果是true。
!= 不等于符号 ,如果符号两边的数据不相等,结果是true。
  • 比较运算符,是两个数据之间进行比较的运算,运算结果都是布尔值 true 或者 false
1
2
3
4
5
6
7
8
public static void main(String[] args) {
System.out.println(1==1);//true
System.out.println(1<2);//true
System.out.println(3>4);//false
System.out.println(3<=4);//true
System.out.println(3>=4);//false
System.out.println(3!=4);//true
}
  • 注意事项:

    1.比较运算符的结果一定是一个boolean值,成立就是true,不成立就是false

    2.如果进行多次判断,不能连着写。
    数学当中的写法,例如:1 < x < 3
    程序当中【不允许】这种写法。

逻辑运算符

逻辑运算符包括:
&& 短路与 1. 两边都是true,结果是true
2. 一边是false,结果是false
短路特点:符号左边是false,右边不再运算
`
取反 1. ! true 结果是false
2. ! false结果是true
  • 逻辑运算符,是用来连接两个布尔类型结果的运算符,运算结果都是布尔值 true 或者 false
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
System.out.println(true && true);//true
System.out.println(true && false);//false
System.out.println(false && true);//false,右边不计算
System.out.println(false || false);//falase
System.out.println(false || true);//true
System.out.println(true || false);//true,右边不计算
System.out.println(!false);//true
}

  • 注意事项:

    1.逻辑运算符只能用于boolean值。

    2.与、或需要左右各自有一个boolean值,但是取反只要有唯一的一个boolean值即可。

    3.与、或两种运算符,如果有多个条件,可以连续写。
    两个条件:条件A && 条件B
    多个条件:条件A && 条件B && 条件C

三元运算符

  • 三元运算符格式:
1
数据类型 变量名 = 布尔类型表达式?结果1:结果2
  • 三元运算符计算方式:
    • 布尔类型表达式结果是true,三元运算符整体结果为结果1,赋值给变量。
    • 布尔类型表达式结果是false,三元运算符整体结果为结果2,赋值给变量。
1
2
3
4
5
6
public static void main(String[] args) {
int i = (1==2 ? 100 : 200);
System.out.println(i);//200
int j = (3<=4 ? 500 : 600);
System.out.println(j);//500
}

方法入门


概述

我们在学习运算符的时候,都为每个运算符单独的创建一个新的类和main方法,我们会发现这样编写代码非常的繁琐,而且 重复的代码过多。能否避免这些重复的代码呢,就需要使用方法来实现。

  • 方法:就是将一个功能抽取出来,把代码单独定义在一个大括号内,形成一个单独的功能。

当我们需要这个功能的时候,就可以去调用。这样即实现了代码的复用性,也解决了代码冗余的现象。

方法的定义

  • 定义格式:
1
2
3
4
修饰符 返回值类型 方法名 (参数列表){
代码...
return ;

  • 定义格式解释:

    • 修饰符:目前固定写法 public static
    • 返回值类型:目前固定写法 void
    • 方法名:为我们定义的方法起名,满足标识符的规范,用来调用方法。
    • 参数列表: 目前无参数, 带有参数的方法在后面的课程讲解。
    • return:方法结束。因为返回值类型是void,方法大括号内的return可以不写。
  • 举例:

1
2
3
public static void methodName() {
System.out.println("这是一个方法");
}

方法的调用

方法在定义完毕后,方法不会自己运行,必须被调用才能执行,我们可以在主方法main中来调用我们自己定义好的方法。在 主方法中,直接写要调用的方法名字就可以调用了。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
//调用定义的方法method
method();
}
//定义方法,被main方法调用
public static void method() {
System.out.println("自己定义的方法,需要被main调用运行");
}

注意事项

  • 方法定义注意事项:
    • 方法必须定义在一类中方法外。
    • 方法不能定义在另一个方法的里面。

流程控制语句

流程控制


概述

在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。也就是说,程序的流程对运行结果 有直接的影响。所以,我们必须清楚每条语句的执行流程。而且,很多时候我们要通过控制语句的执行顺序来实现 我们要完成的功能。

顺序结构

1
2
3
4
5
6
public static void main(String[] args){
//顺序执行,根据编写的顺序,从上到下运行
System.out.println(1);
System.out.println(2);
System.out.println(3);
}

判断语句


判断语句1–if

  • if语句第一种格式:
1
2
3
if(关系表达式){
语句体;

  • 执行流程
    • 首先判断关系表达式看结果是true还是 false
    • 如果是true就执行语句体
    • 如果是false就不执行

判断语句2–if…else

  • if语句第二种格式: if…else
1
2
3
4
5
if(关系表达式){
语句体A;
}else {
语句体B;
}
  • 执行流程
    • 首先判断关系表达式看起结果是true还是false
    • 如果是true就执行语句体A
    • 如果是false就执行语句体B

3.2.3 判断语句3–if..else if…else

  • if语句第三种格式: if…else if …else
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;
}
  • 执行流程
    • 首先判断关系表达式1看其结果是true还是false
    • 如果是true就执行语句体1
    • 如果是false就继续判断关系表达式2看其结果是true还是false
    • 如果是true就执行语句体2
    • 如果是false就继续判断关系表达式…看其结果是true还是false
    • 如果没有任何关系表达式为true,就执行语句体n+1。

选择语句


选择语句–switch

  • switch语句格式:
1
2
3
4
5
6
7
8
9
10
11
12
switch(表达式) {
case 常量值1:
语句体1;
break;
case 常量值2:
语句体2;
break;
...
default:
语句体n+1;
break;
}
  • 执行流程
    • 首先计算出表达式的值
    • 其次,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结 束。
    • 最后,如果所有的case都和表达式的值不匹配,就会执行default语句体部分,然后程序结束掉。

小提示

switch语句中,表达式的数据类型,可以是byte,short,int,char,enum(枚举),JDK7后可以接收字符串。

case的穿透性

在switch语句中,如果case的后面不写break,将出现穿透现象,也就是不会在判断下一个case的值,直接向后运 行,直到遇到break,或者整体switch结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
int i = 5;
switch (i){
case 0:
System.out.println("执行case0");
break;
case 5:
System.out.println("执行case5");
case 10:
System.out.println("执行case10");
default:
System.out.println("执行default");
}
}

上述程序中,执行case5后,由于没有break语句,程序会一直向后走,不会在判断case,也不会理会break,直接 运行完整体switch。

==由于case存在穿透性,因此初学者在编写switch语句时,必须要写上break。==

循环语句


循环概述

循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体语句,当反复 执行这个循环体时,需要在合适的时候把循环判断条件修改为false,从而结束循环,否则循环将一直执行下去,形成死循环。

循环语句1–for

  • for循环语句格式:
1
2
3
for(初始化表达式①; 布尔表达式②; 步进表达式④){
循环体③
}
  • 执行流程
    • 执行顺序:①②③④>②③④>②③④…②不满足为止。
    • ①负责完成循环变量初始化
    • ②负责判断是否满足循环条件,不满足则跳出循环
    • ③具体执行的语句
    • ④循环后,循环条件所涉及变量的变化情况

循环语句2–while

  • while循环语句格式:
1
2
3
4
5
初始化表达式①
while(布尔表达式②){
循环体③
步进表达式④
}
  • 执行流程
    • 执行顺序:①②③④>②③④>②③④…②不满足为止。
    • ①负责完成循环变量初始化。
    • ②负责判断是否满足循环条件,不满足则跳出循环。
    • ③具体执行的语句。
    • ④循环后,循环变量的变化情况。

循环语句3–do…while

  • do…while循环格式
1
2
3
4
5
初始化表达式①
do{
循环体③
步进表达式④
}while(布尔表达式②);
  • 执行流程
    • 执行顺序:①③④>②③④>②③④…②不满足为止。
    • ①负责完成循环变量初始化。
    • ②负责判断是否满足循环条件,不满足则跳出循环。
    • ③具体执行的语句
    • ④循环后,循环变量的变化情况

循环语句的区别

  • forwhile 的小区别:
    • 控制条件语句所控制的那个变量,在for循环结束后,就不能再被访问到了,而while循环结束还可以继 续使用,如果你想继续使用,就用while,否则推荐使用for。原因是for循环结束,该变量就从内存中消 失,能够提高内存的使用效率。
    • 在已知循环次数的时候使用推荐使用for,循环次数未知的时推荐使用while

跳出循环

==break==

  • 使用场景:终止switch或者循环
    • 在选择结构switch语句中
    • 在循环语句中
    • 离开使用场景的存在是没有意义的

==continue==

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

扩展知识点


死循环

  • 死循环:也就是循环中的条件永远为true,死循环的是永不结束的循环。例如:while(true){}

在后期的开发中,会出现使用死循环的场景,例如:我们需要读取用户输入的输入,但是用户输入多少数据我们并 不清楚,也只能使用死循环,当用户不想输入数据了,就可以结束循环了,如何去结束一个死循环呢,就需要使用 到跳出语句了。

嵌套循环

  • 所谓嵌套循环,是指一个循环的循环体是另一个循环。比如for循环里面还有一个for循环,就是嵌套循环。总 共的循环次数=外循环次数*内循环次数
  • 嵌套循环格式:
1
2
3
4
5
for(初始化表达式①; 循环条件②; 步进表达式⑦) {
for(初始化表达式③; 循环条件④; 步进表达式⑥) {
执行语句⑤;
}
}
  • 嵌套循环执行流程:
    • 执行顺序:①②③④⑤⑥>④⑤⑥>⑦②③④⑤⑥>④⑤⑥
    • 外循环一次,内循环多次。
    • 比如跳绳:一共跳5组,每组跳10个。5组就是外循环,10个就是内循环。

方法

IDEA


IDEA常用快捷键

快捷键 功能
Alt+Enter 导入包,自动修正代码
Ctrl+Y 删除光标所在行
Ctrl+D 复制光标所在行的内容,插入光标位置下面
Ctrl+Alt+L 格式化代码
Ctrl+/ 单行注释
Ctrl+Shift+/ 选中代码注释,多行注释,再按取消注释
Alt+Ins 自动生成代码,toString,get,set等方法
Alt+Shift+上下箭头 移动当前代码行

方法


回顾–方法的定义和调用

前面的课程中,使用过嵌套循环输出矩形,控制台打印出矩形就可以了,因此将方法定义为 void ,没有返回值。 在主方法 main 中直接被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Method_Demo1 {
public static void main(String[] args) {
print();
}
private static void print() {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 8; j++) {
System.out.print("*");
}
System.out.println();
}
}
}

print 方法被 main 方法调用后直接输出结果,而 main 方法并不需要 print 方法的执行结果,所以被定义为 void

定义方法的格式详解

1
2
3
4
修饰符 返回值类型 方法名(参数列表){
//代码省略...
return 结果;
}
  • 修饰符: public static 固定写法
  • 返回值类型: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者
  • 参数列表:方法在运算过程中的未知数据,调用者调用方法时传递
  • return:将方法执行后的结果带给调用者,方法执行到 return ,整体方法运行结束

小贴士:return 结果; 这里的”结果”在开发中,我们正确的叫法成为方法的返回值

定义方法的两个明确

  • 需求:定义方法实现两个整数的求和计算。
    • 明确返回值类型:方法计算的是整数的求和,结果也必然是个整数,返回值类型定义为int类型。
    • 明确参数列表:计算哪两个整数的和,并不清楚,但可以确定是整数,参数列表可以定义两个int类型的 变量,由调用者调用方法时传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Method_Demo2 {
public static void main(String[] args) {
// 调用方法getSum,传递两个整数,这里传递的实际数据又称为实际参数
// 并接收方法计算后的结果,返回值
int sum = getSum(5, 6);
System.out.println(sum);
}
/*定义计算两个整数和的方法
返回值类型,计算结果是int
参数:不确定数据求和,定义int参数.参数又称为形式参数
*/
public static int getSum(int a, int b) {
return a + b;
}
}

程序执行,主方法 main 调用 getSum 方法,传递了实际数据 56 ,两个变量 ab 接收到的就是实际参数,并 将计算后的结果返回,主方法 main 中的变量 sum 接收的就是方法的返回值。

调用方法的流程图解

  • 方法的三种调用格式

    1. 单独调用方法名称(参数);
    2. 打印调用System.out.println(方法名称(参数));
    3. 赋值调用数据类型 变量名称 = 方法名称(参数);
  • 注意:此前学习的方法,返回值类型固定写为 void ,这种方法只能单独调用,不能进行打印调用或者赋值调用。

  • 有参数:小括号当中有内容,当一个方法需要一些数据条件,才能完成任务的时候,就是有参数。

    • 例如俩个数字相加,必须知道俩个数字各自是多少,才能相加。
  • 无参数:小括号当中无内容,一个方法不需要任何数据条件,自己就能独立完成任务,就是无参数。

    • 例如定义一个方法,打印固定10次HelloWorld。

  • 注意事项
    • 对于有返回值的方法,可以使用单独调用、打印调用或者赋值调用。
    • 对于无返回值的方法,只能使用单独调用,不能使用打印调用或者赋值调用。

方法的注意事项

  1. 方法应该定义在类当中,但是不能在方法当中再定义方法。不能嵌套。
  2. 方法定义的前后顺序无所谓。
  3. 方法定义之后不会执行,如果希望执行,一定要调用:单独调用、打印调用、赋值调用。
  4. 如果方法有返回值,那么必须写上 return 返回值 ,不能没有。
  5. return 后面的数据,必须和方法的返回值类型,对应起来
  6. 对于应该void 没有返回值的方法,不能写 return 后面的返回值,只能写 return 自己。
  7. 对于 void 方法当中最后一行的 return 可以省略不写。
  8. 一个方法当中可以有多个 return 语句,但是必须保证同时只有一个会被执行到,俩个 return 不能连写。

方法重载

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

  • 参数列表:个数不同,数据类型不同,顺序不同。

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

  • 方法重载与下列因素相关

    1. 参数个数不同
    2. 参数类型不同
    3. 参数的多类型顺序不同
  • 方法重载与下列因素无关

    1. 与参数的名称无关
    2. 与方法的返回值类型无关

数组

数组定义和访问


数组概念

  • 数组概念: 数组就是存储数据长度固定的容器,保证多个数据的数据类型要一致。
  • 数组的特点
    1. 数组是一种引用数据类型
    2. 数组当中的多个数据,类型必须统一
    3. 数组的长度在程序运行期间不可改变

数组的定义

  • 数组的初始化:在内存当中创建一个数组,并且向其他赋予一些默认值。

  • 俩种常见初始化方式

    1. 动态初始化(指定长度):在创建数组的时候,直接指定数组当中的数据元素个数。
    2. 静态初始化(指定内容):在创建数组的时候,不直接指定数据个数多少,而是直接将具体的数据内容进行指定。
  • 动态初始化数组的格式

1
数据类型[] 数组名称 = new 数据类型[数组长度];
  • 数组定义格式详解:

    • 左边数据类型: 创建的数组容器可以存储什么数据类型。
    • [] : 表示数组。
    • 数组名字:为定义的数组起个变量名,满足标识符规范,可以使用名字操作数组。
    • new:关键字,创建数组使用的关键字。
    • 右边数据类型: 创建的数组容器可以存储什么数据类型,必须和左边的数据类型保持一致。
    • [数组长度]:数组的长度,表示数组容器中可以存储多少个元素,是一个 int 数字。
    • 注意:数组有定长特性,长度一旦指定,不可更改。
      • 和水杯道理相同,买了一个2升的水杯,总容量就是2升,不能多也不能少。
  • 静态初始化数组的格式

1
数据类型[] 数组名称 = new 数据类型[] {元素1,元素2,... };//标准格式

使用静态初始化数组的时候,格式还可以省略一下。

1
数据类型[] 数组名称 = {元素1,元素2,... };//省略格式
  • 注意事项:

    1. 静态初始化没有直接指定长度,但是仍然会自动推算得到长度。
    2. 静态初始化标准格式可以拆分为俩个步骤。
    3. 动态初始化也可以拆分成为俩个步骤。
    4. 静态初始化一旦使用省略格式,就不能拆分成为俩个步骤了。
  • 使用建议:如果不确定数组当中的具体内容,用动态初始化;否则,已经确定了具体的内容,用静态初始化。

数组的访问

==【注意】直接打印数组名称,得到的是数组对应的:内存哈希值。==

  • 索引:每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为**数组索引 (index)**,可以通过数组的索引访问到数组中的元素。

  • 索引值:就是一个 int 数字,代表数组当中元素的编号。

    【注意】索引值从0开始,一直到“数组的长度-1”为止。

  • 格式

1
数据名[索引值]
  • 数组的长度属性: 每个数组都具有长度,而且是固定的,Java中赋予了数组的一个属性,可以获取到数组的 长度,语句为: 数组名.length ,属性length的执行结果是数组的长度,int类型结果。由次可以推断出,数 组的最大索引值为 数组名.length-1

  • 索引访问数组中的元素

    • 数组名[索引]=数值,为数组中的元素赋值
    • 变量=数组名[索引],获取出数组中的元素
  • 使用动态初始化数组的时候,其中的元素会自动拥有一个默认值。规则如下:

    • 如果是整数类型,那么默认为 0
    • 如果是浮点类型,那么默认为 0.0
    • 如果是字符类型,那么默认为 \u0000;
    • 如果是布尔类型,那么默认为 false
    • 如果是引用类型,那么默认为 null

==【注意事项】静态初始化其实也有默认值的过程,只不过系统自动将默认值替换成为了大括号中的具体数值。==

数组原理内存图


内存概述

内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程 序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。

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

Java虚拟机的内存划分

为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

  • VM的内存划分:

    区域名称 作用
    寄存器 给CPU使用,和我们开发无关。
    本地方法栈 JVM在使用操作系统功能的时候使用,和我们开发无关
    方法区 存储可以运行的class文件。
    堆内存 存储对象或者数组,new来创建的,都存储在堆内存。
    方法栈 方法运行时使用的内存,比如main方法运行,进入方法栈中执行。

数组在内存中的存储

一个数组的内存图

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

以上方法执行,输出结果是 ==[I@5f150435== ,这是个什么呢?是数组在内存中的地址。new 出来的内容,都是在堆内存中存储的,而方法中的变量arr 保存的数组的地址。

输出 arr[0] ,就会输出 arr 保存的内存地址中数组中0索引上的元素

两个数组内存图

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

两个变量指向一个数组

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

数组的常见操作


数组越界异常

观察一下代码,运行后会出现什么结果。

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

创建数组,赋值3个元素,数组的索引就是0,1,2,没有3索引,因此我们不能访问数组中不存在的索引,程序运行后,将会抛出 ArrayIndexOutOfBoundsException 数组越界异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。

数组空指针异常

观察一下代码,运行后会出现什么结果。

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

arr = null 这行代码,意味着变量arr将不会在保存数组的内存地址,也就不允许再操作数组了,因此运行的时候会抛出 NullPointerException 空指针异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。

解决:补上new

空指针异常在内存图中的表现

数组遍历【重点】

  • 数组遍历 :就是将数组中的每个元素分别获取出来,就是遍历。遍历也是数组操作中的基石。
1
2
3
4
5
6
7
8
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
System.out.println(arr[4]);
}

以上代码是可以将数组中每个元素全部遍历出来,但是如果数组元素非常多,这种写法肯定不行,因此我们需要改造成循环的写法。数组的索引是0lenght-1 ,可以作为循环的条件出现。

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

数组获取最大值元素

  • 最大值获取:从数组的所有元素中找出最大值。、
  • 实现思路:
    • 定义变量,保存数组0索引上的元素
    • 遍历数组,获取出数组中的每个元素
    • 将遍历到的元素和保存数组0索引上值的变量进行比较
    • 如果数组元素的值大于了变量的值,变量记录住新的值
    • 数组循环遍历结束,变量保存的就是数组中的最大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
int[] arr = { 5, 15, 2000, 10000, 100, 4000 };
//定义变量,保存数组中0索引的元素
int max = arr[0];
//遍历数组,取出每个元素
for (int i = 0; i < arr.length; i++) {
//遍历到的元素和变量max比较
//如果数组元素大于max
if (arr[i] > max) {
//max记录住大值
max = arr[i];
}
}
System.out.println("数组最大值是: " + max);
}

数组反转

  • 数组的反转: 数组中的元素颠倒顺序,例如原始数组为1,2,3,4,5,反转后的数组为5,4,3,2,1
  • 实现思想:数组最远端的元素互换位置。
    • 实现反转,就需要将数组最远端元素位置交换
    • 定义两个变量,保存数组的最小索引和最大索引
    • 两个索引上的元素交换位置
    • 最小索引++,最大索引–,再次交换位置
    • 最小索引超过了最大索引,数组反转操作结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
/*
循环中定义变量min=0最小索引
max=arr.length‐1最大索引
min++,max‐‐
*/
for (int min = 0, max = arr.length ‐ 1; min <= max; min++, max‐‐) {
//利用第三方变量完成数组中的元素交换
int temp = arr[min];
arr[min] = arr[max];
arr[max] = temp;
}
// 反转后,遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

数组作为方法参数和返回值


数组作为方法参数

以前的方法中我们学习了方法的参数和返回值,但是使用的都是基本数据类型。那么作为引用类型的数组能否作为方法的参数进行传递呢,当然是可以的。

  • 数组作为方法参数传递,传递的参数是数组内存的地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
int[] arr = { 1, 3, 5, 7, 9 };
//调用方法,传递数组
printArray(arr);
}
/*
创建方法,方法接收数组类型的参数
进行数组的遍历
*/
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

数组作为方法返回值

  • 数组作为方法的返回值,返回的数组的内存地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
//调用方法,接收数组的返回值
//接收到的是数组的内存地址
int[] arr = getArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
/*
创建方法,返回值是数组类型
return返回数组的地址
*/
public static int[] getArray() {
int[] arr = { 1, 3, 5, 7, 9 };
//返回数组的地址,返回到调用者
return arr;
}

方法的参数类型区别

代码分析

1.分析下列程序代码,计算输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int a = 1;
int b = 2;
System.out.println(a);
System.out.println(b);
change(a, b);
System.out.println(a);
System.out.println(b);
}
public static void change(int a, int b) {
a = a + b;
b = b + a;
}

2. 分析下列程序代码,计算输出结果。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int[] arr = {1,3,5};
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] arr) {
arr[0] = 200;
}

总结:

方法的参数为基本类型时,传递的是数据值. 方法的参数为引用类型时,传递的是地址值。

类与对象、封装、构造方法

面向对象思想


面向对象思想概述

概述

Java语言是一种面向对象的程序设计语言,而面向对象思想是一种程序设计思想,我们在面向对象思想的指引下,使用Java语言去设计、开发计算机程序。 这里的对象泛指现实中一切事物,每种事物都具备自己的属性和行为。面向对象思想就是在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,描述成计算机事件的设计思想。 它区别于面向过程思想,强调的是通过调用对象的行为来实现功能,而不是自己一步一步的去操作实现。

举例

洗衣服:

  • 面向过程:把衣服脱下来–>找一个盆–>放点洗衣粉–>加点水–>浸泡10分钟–>揉一揉–>清洗衣服–>拧干–>晾起来
  • 面向对象:把衣服脱下来–>打开全自动洗衣机–>扔衣服–>按钮–>晾起来

区别:

  • 面向过程:强调步骤。
  • 面向对象:强调对象,这里的对象就是洗衣机。

特点

面向对象思想是一种更符合我们思考习惯的思想,它可以将复杂的事情简单化,并将我们从执行者变成了指挥者。面向对象的语言中,包含了三大基本特征,即封装、继承和多态。

类与对象

环顾周围,你会发现很多对象,比如桌子,椅子,同学,老师等。桌椅属于办公用品,师生都是人类。那么什么是类呢?什么是对象呢?

什么是类

  • 类:是一组相关属性行为的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物。

现实中,描述一类事物:

  • 属性:就是该事物的状态信息。
  • 行为:就是该事物能够做什么。

举例:小猫。

属性:名字、体重、年龄、颜色。 行为:走、跑、叫。

什么是对象

  • 对象:是一类事物的具体体现。对象是类的一个实例(对象并不是找个女朋友),必然具备该类事物的属性和行为。

现实中,一类事物的一个实例:一只小猫。

举例:一只小猫。

属性:tom、5kg、2 years、yellow。 行为:溜墙根走、蹦跶的跑、喵喵叫。

类与对象的关系

  • 类是对一类事物的描述,是抽象的。
  • 对象是一类事物的实例,是具体的。
  • 类是对象的模板,对象是类的实体。

类的定义

事物与类的对比

现实世界的一类事物:

  • 属性:事物的状态信息。
  • 行为:事物能够做什么。

Java中用class 描述事物也是如此:

  • 成员变量:对应事物的属性
  • 成员方法:对应事物的行为

类的定义格式

1
2
3
4
public class ClassName{
//成员变量
//成员方法
}
  • 定义类:就是定义类的成员,包括成员变量成员方法
  • 成员变量:和以前定义变量几乎是一样的。只不过位置发生了改变。在类中,方法外
  • 成员方法:和以前定义方法几乎是一样的。只不过static去掉static的作用在面向对象后面课程中再详细讲解。

【注意事项】

  1. 成员变量是直接定义在类当中的,在方法外边。
  2. 成员变量不要写static关键字。

对象的使用

对象的使用格式

创建对象:

1
类名 对象名 = new 类名();

使用对象访问类中的成员:

1
2
对象名.成员变量;
对象名.成员方法();

成员变量的默认值

如果成员变量没有进行赋值,那么将会有一个默认值,规则和数组一样,如下表。

数据类型 默认值
基本类型 整数(byteshortintlong 0
浮点数(floatdouble 0.0
字符(char '\u0000'
布尔(boolean false
引用类型 数组,类,接口,字符串(Stirng null

对象内存图

一个对象,调用一个方法内存图

通过上图,我们可以理解,在栈内存中运行的方法,遵循”先进后出,后进先出”的原则。变量p指向堆内存中的空间,寻找方法信息,去执行该方法。

但是,这里依然有问题存在。创建多个对象时,如果每个对象内部都保存一份方法信息,这就非常浪费内存了,因为所有对象的方法信息都是一样的。那么如何解决这个问题呢?请看如下图解。

两个对象,调用一个方法内存图

对象调用方法是,根据对象中的方法标记(地址值),去类中寻找方法信息。这样哪怕是多个对象,方法信息只保存一份,节约内存空间。

一个引用,作为参数传递到方法中内存图

引用类型作为参数,传递的是地址值。

成员变量和局部变量区别

变量根据定义位置的不同,给变量起了不同的名字。如下图所示:

  • 在类中的位置不同 ==【重点】==
    • 成员变量:类中,方法外
    • 局部变量:方法中或者方法声明上(形式参数)
  • 作用范围不一样 ==【重点】==
    • 成员变量:类中
    • 局部变量:方法中
  • 初始化值的不同 ==【重点】==
    • 成员变量:有默认值
    • 局部变量:没有默认值。必须先定义,赋值,最后使用
  • 在内存中的位置不同
    • 成员变量:堆内存
    • 局部变量:栈内存
  • 生命周期不同
    • 成员变量:随着对象的创建而存在,随着对象的消失而消失
    • 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失

封装


封装概述

概述

面向对象编程语言是对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界无法直接操作和修改。封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。要访问该类的数据,必须通过指定的方式。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性

原则

属性隐藏起来,若需要访问某个属性,提供公共方法对其访问。

封装的步骤

  1. 使用 private 关键自来修饰成员变量。
  2. 对需要访问的成员变量,提供对应的一对 getXxx 方法、setXxx 方法。

封装的操作—— private 关键字


private的含义

  1. private 是一个权限修饰符,代表最小权限。
  2. 可以修饰成员变量和成员方法。
  3. private 修饰后的成员变量和成员方法,只在本类中才能访问。

private的使用格式

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

    1
    2
    3
    4
    public class Student {
    private String name;
    private int age;
    }
  2. 提供 getXxx 方法 / setXxx 方法,可以访问成员变量,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Student {
    private String name;
    private int age;

    public void setName(String n) {
    name = n;
    }
    public String getName() {
    return name;
    }
    public void setAge(int a) {
    age = a;
    }
    public int getAge() {
    return age;
    }
    }

封装优化1——this关键字


我们发现 setXxx 方法中的形参名字并不符合见名知意的规定,那么如果修改与成员变量名一致,是否就见名知意了呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
private String name;
private int age;

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

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

经过修改和测试,我们发现新的问题,成员变量赋值失败了。也就是说,在修改了setXxx() 的形参变量名后,方法并没有给成员变量赋值!这是由于形参变量名与成员变量名重名,导致成员变量名被隐藏,方法中的变量名,无法访问到成员变量,从而赋值失败。所以,我们只能使用this关键字,来解决这个重名问题。

this的含义

this 代表所在类的当前对象的引用(地址值),即对象自己的引用。

记住 :方法被哪个对象调用,方法中的this就代表那个对象。即谁在调用,this就代表谁。

this使用格式

1
this.成员变量名

使用 this 修饰方法中的变量,解决成员变量被隐藏的问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Student {
private String name;
private int age;

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

public String getName() {
return name;
}
public void setAge(int age) {
//age = age;
this.age = age;
}

public int getAge() {
return age;
}
}

小贴士:方法中只有一个变量名时,默认也是使用 this 修饰,可以省略不写。

封装优化2——构造方法

当一个对象被创建时候,构造方法用来初始化该对象,给对象的成员变量赋初始值

小贴士:无论你与否自定义构造方法,所有的类都有构造方法,因为Java自动提供了一个无参数构造方法,
一旦自己定义了构造方法,Java自动提供的默认无参数构造方法就会失效。

构造方法的定义格式

1
2
3
修饰符 构造方法名(参数列表){
// 方法体
}

构造方法的写法上,方法名与它所在的类名相同。它没有返回值,所以不需要返回值类型,甚至不需要void。使用
构造方法后,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
private String name;
private int age;

// 无参数构造方法
public Student() {}

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

【注意事项】

  1. 构造方法的名称必须和所在的类名称完全一样,就连大小写也要一样
  2. 构造方法不要写返回值类型,连void都不写
  3. 构造方法不能return一个具体的返回值
  4. 如果没有编写任何构造方法,那么编译器将会默认赠送一个构造方法,没有参数、方法体什么事情都不做。
    public Student() {}
  5. 一旦编写了至少一个构造方法,那么编译器将不再赠送。
  6. 构造方法也是可以进行重载的。
    重载:方法名称相同,参数列表不同。

标准代码——JavaBean

JavaBean 是 Java语言编写类的一种标准规范。符合JavaBean 的类,要求类必须是具体的和公共的,并且具有无参数的构造方法,提供用来操作成员变量的setget 方法。

一个标准的类通常要拥有下面四个组成部分:

  1. 所有的成员变量都要使用private关键字修饰
  2. 为每一个成员变量编写一对儿Getter/Setter方法
  3. 编写一个无参数的构造方法
  4. 编写一个全参数的构造方法

这样标准的类也叫做JavaBean

1
2
3
4
5
6
7
8
9
public class ClassName{
//成员变量
//构造方法
//无参构造方法【必须】
//有参构造方法【建议】
//成员方法
//getXxx()
//setXxx()
}

编写符合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
28
29
30
public class Student {
//成员变量
private String name;
private int age;

//构造方法
public Student() {}

public Student(String name,int age) {
this.name = name;
this.age = age;
}

//成员方法
publicvoid setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

publicvoid setAge(int age) {
this.age = age;
}

publicint getAge() {
return age;
}
}

测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class TestStudent {
public static void main(String[] args) {
//无参构造使用
Student s= new Student();
s.setName("柳岩");
s.setAge(18);
System.out.println(s.getName()+"‐‐‐"+s.getAge());
//带参构造使用
Student s2= new Student("赵丽颖",18);
System.out.println(s2.getName()+"‐‐‐"+s2.getAge());
}
}

常用类

API


概述

API(Application Programming Interface),应用程序编程接口。Java API是一本程序员的字典 ,是JDK中提供给我们使用的类的说明文档。这些类将底层的代码实现封装了起来,我们不需要关心这些类是如何实现的,只需要学习这些类如何使用即可。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。

API使用步骤

  1. 打开帮助文档。
  2. 点击显示,找到索引,看到输入框。
  3. 你要找谁?在输入框里输入,然后回车。
  4. 看包。java.lang下的类不需要导包,其他需要。
  5. 类的解释和说明。
  6. 学习构造方法。
  7. 使用成员方法。

Scanner


了解了API的使用方式,我们通过Scanner类,熟悉一下查询API,并使用类的步骤。

什么是Scanner

一个可以解析基本类型和字符串的简单文本扫描器。 例如,以下代码使用户能够从 System.in 中读取一个数:

1
2
Scanner sc =  new Scanner(System.in);
int i = sc.nextInt();

备注:System.in 系统输入指的是通过键盘录入数据。

引用类型使用步骤

导包

使用 import 关键字导包,在类的所有代码之前导包,引入要使用的类型,java.lang包下的所有类无需导入。 格式:

1
import 包名.类名;

创建对象

使用该类的构造方法,创建一个该类的对象。格式:

1
数据类型 变量名 = new 数据类型(参数列表);

调用方法

调用该类的成员方法,完成指定功能。格式:

1
变量名.方法名();

Scanner使用步骤

查看类

  • java.util.Scanner :该类需要 import 导入后使用。

查看构造方法

  • public Scanner(InputStream source) :构造一个新的 Scanner,它生成的值是从指定的输入流扫描的。

查看成员方法

  • public int nextInt():将输入信息的下一个标记扫描为一个 int 值。

使用 Scanner 类,完成接收键盘录入数据的操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1.导包
import java.util.Scanner;
public class Demo01_Scanner{
public static void main(String[] args){
//2.创建键盘录入数据的对象
Scanner sc = new Scanner(System.in);

//3.接收数据
System.out.println("请录入一个整数:");
int i = sc.naxtInt();

//4.输出数据
System.out.println("i:" + i);
}
}

匿名对象

概念

创建对象时,只有创建对象的语句,却没有把对象地址值赋值给某个变量。虽然是创建对象的简化写法,但是应用
场景非常有限。

  • 匿名对象:没有变量名的对象。

格式:

1
new 类名(参数列表);

举例:

1
new Scanner(System.in);

应用场景

  1. 创建匿名对象直接调用方法,没有变量名。

    1
    new Scanner(System.in).nextInt();
  2. 一旦调用俩次打打,就是创建了俩个对象,造成浪费。

    1
    2
    new Scanner(System.in).nextInt();
    new Scanner(System.in).nextInt();

    注意事项:匿名对象只能使用唯一的一次,下次再用不得不再创建一个新对象。

    使用建议:如果确定有一个对象只需要使用唯一的一次,就可以用匿名对象。

  3. 匿名对象可以作为方法的参数和返回值

  • 作为参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Test {
    public static void main(String[] args) {
    // 普通方式
    Scanner sc = new Scanner(System.in);
    input(sc);
    //匿名对象作为方法接收的参数
    input(new Scanner(System.in));
    }
    public static void input(Scanner sc){
    System.out.println(sc);
    }
    }
  • 作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test2 {
    public static void main(String[] args) {
    // 普通方式
    Scanner sc = getScanner();
    }
    public static Scanner getScanner(){
    //普通方式
    //Scanner sc = new Scanner(System.in);
    //return sc;
    //匿名对象作为方法返回值
    return new Scanner(System.in);
    }
    }

Random类


什么是Random类

此类的实例用于生成伪随机数。

例如,以下代码使用户能够得到一个随机数:

1
2
Random r = new Random();
int i = r.nextInt();

Random使用步骤

查看类

  • java.util.Random:该类需要import导入后使用。

查看构造方法

  • public Random():创建一个新的随机数生成器。

查看成员方法

  • public int nextInt(int n):返回一个伪随机数,范围在0包括)和指定值n(不包括)之间的int值。

备注:创建一个Random对象,每次调用nextInt()方法,都会生成一个随机数。

ArrayList类


什么是ArrayList类

java.util.ArrayList是大小可变的数组的实现,存储在内的数据称为元素。此类提供一些方法来操作内部存储的元素。 ArrayList中可不断添加元素,其大小也自动增长。

ArrayList使用步骤

查看类

  • java.util.ArrayList<E>:构造一个内容为空的集合。

基本格式:

1
ArrayList<String> list = new Arraylist<String>();

在JDK 7后,右侧泛型的尖括号之内可以留空,但是<>仍然要写。简化格式:

1
ArrayList<String> list = new Arraylist<();

查看成员方法

  • public boolean add(E e) : 将指定的元素添加到此集合的尾部。

    参数E e,在构造ArrayList对象时, <E>指定了什么数据类型,那么add(E e)方法中,只能添加什么数据类型的对象。

常用方法和遍历

对于元素的操作,基本体现在——增、删、查。常用的方法有:

  • public boolean add(E e):将指定的元素添加到此集合的尾部。
  • public E remove(int index):移除此集合中指定位置上的元素。返回被删除的元素。
  • public E get(int index) :返回此集合中指定位置上的元素。返回获取的元素。
  • public int size():返回此集合中的元素数。遍历集合时,可以控制索引范围,防止越界。
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 Demo01ArrayListMethod {
public static void main(String[] args) {
//创建集合对象
ArrayList<String> list = new ArrayList<String>();

//添加元素
list.add("hello");
list.add("world");
list.add("java");

//public E get(int index):返回指定索引处的元素
System.out.println("get:"+list.get(0));
System.out.println("get:"+list.get(1));
System.out.println("get:"+list.get(2));

//public int size():返回集合中的元素的个数
System.out.println("size:"+list.size());

//public E remove(int index):删除指定索引处的元素,返回被删除的元素
System.out.println("remove:"+list.remove(0));

//遍历输出
for(int i = 0; i < list.size(); i++){
System.out.println(list.get(i));
}
}
}

如何存储基本数据类型

ArrayList对象不能存储基本类型,只能存储引用类型的数据。类似 <int> 不能写,但是存储基本数据类型对应的包装类型是可以的。所以,想要存储基本类型数据, <> 中的数据类型,必须转换后才能编写,转换写法如下:

基本类型 基本类型包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

我们发现,只有 IntegerCharacter 需要特殊记忆,其他基本类型只是首字母大写即可。那么存储基本类型数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class Demo02ArrayListMethod {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

System.out.println(list);
}
}

String类


String类概述

概述

java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如 "abc" )都可以别看作是实现此类的实例.

String 中包括用于检查个字符串的方法,比如用于比较字符串,搜素字符串,提取子字符串以及创建具有翻译为大写小写的所有字符的字符串副本。

特点

  1. 字符串不变 :字符串的值在创建后不能被更改。

    1
    2
    3
    4
    String s1 = "abc";
    s1 += "d";
    System.out.println(s1);//"abcd"
    //内存中有"abc","abcd"俩个对象,s1从指向"abc",改变指向,指向了"abcd"。
  2. 因为String对象是不可变的,所以它们可以被共享。

    1
    2
    3
    String s1 = "abc";
    String s2 = "abc";
    //内存中只有一个"abc"对象被创建,同时被s1和s2共享
  3. "abc"等效于char[] data={'a','b','c'}

    1
    2
    3
    4
    5
    6
    7
    例如:
    String str = "abc";

    相当于:
    char[] data = {'a','b','c'};
    String str = new String(data);
    //String底层是靠字符数组实现的。

使用步骤

查看类

  • java.langString:此类不需要导入。

查看构造方法

  • public String() :初始化新创建的String对象,以使其表示空字符序列。

  • public String(char[] value) :通过当前参数中的字符数组来构造新的String。

  • public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。

  • 构造举例,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 无参构造
    String str = new String();

    // 通过字符数组构造
    char chars[] = {'a', 'b', 'c'};
    String str2 = new String(chars);

    // 通过字节数组构造
    byte bytes[] = { 97, 98, 99 };
    String str3 = new String(bytes);

常用方法

判断功能的方法

  • public boolean equals(Object anObject) :将此字符与指定对象进行比较。

  • public Boolean equalsIgnoreCase(String anotherString) :将此字符与指定对象进行比较,忽略大小写。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class String_Demo01 {
    public static void main(String[] args) {
    // 创建字符串对象
    String s1 = "hello";
    String s2 = "hello";
    String s3 = "HELLO";

    // boolean equals(Object obj):比较字符串的内容是否相同
    System.out.println(s1.equals(s2)); // true
    System.out.println(s1.equals(s3)); // false
    System.out.println("‐‐‐‐‐‐‐‐‐‐‐");

    //boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
    System.out.println(s1.equalsIgnoreCase(s2)); // true
    System.out.println(s1.equalsIgnoreCase(s3)); // true
    System.out.println("‐‐‐‐‐‐‐‐‐‐‐");
    }
    }

Object 是” 对象”的意思,也是一种引用类型。作为参数类型,表示任意对象都可以传递到方法中。

获取功能的方法

  • public int length () :返回此字符串的长度。
  • public String concat (String str) :将指定的字符串连接到该字符串的末尾。
  • public char charAt (int index) :返回指定索引处的 char值。
  • public int indexOf (String str) :返回指定子字符串第一次出现在该字符串内的索引。
  • public String substring (int beginIndex) :返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
  • public String substring (int beginIndex, int endIndex) :返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndex。

示例:

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
public class String_Demo02 {
public static void main(String[] args) {
//创建字符串对象
String s = "helloworld";

// int length():获取字符串的长度,其实也就是字符个数
System.out.println(s.length());
System.out.println("‐‐‐‐‐‐‐‐");

// String concat (String str):将将指定的字符串连接到该字符串的末尾.
String s = "helloworld";
String s2 = s.concat("**hello itheima");
System.out.println(s2);// helloworld**hello itheima

// char charAt(int index):获取指定索引处的字符
System.out.println(s.charAt(0));
System.out.println(s.charAt(1));
System.out.println("‐‐‐‐‐‐‐‐");

// int indexOf(String str):获取str在字符串对象中第一次出现的索引,没有返回‐1
System.out.println(s.indexOf("l"));
System.out.println(s.indexOf("owo"));
System.out.println(s.indexOf("ak"));
System.out.println("‐‐‐‐‐‐‐‐");

// String substring(int start):从start开始截取字符串到字符串结尾
System.out.println(s.substring(0));
System.out.println(s.substring(5));
System.out.println("‐‐‐‐‐‐‐‐");

// String substring(int start,int end):从start到end截取字符串。含start,不含end。
System.out.println(s.substring(0, s.length()));
System.out.println(s.substring(3,8));
}
}

转换功能的方法

  • public char[] toCharArray () :将此字符串转换为新的字符数组。
  • public byte[] getBytes () :使用平台的默认字符集将该 String编码转换为新的字节数组。
  • public String replace (CharSequence target, CharSequence replacement) :将与target匹配的字符串使用replacement字符串替换。、

示例:

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 String_Demo03 {
public static void main(String[] args) {
//创建字符串对象
String s = "abcde";

// char[] toCharArray():把字符串转换为字符数组
char[] chs = s.toCharArray();
for(int x = 0; x < chs.length; x++) {
System.out.println(chs[x]);
}
System.out.println("‐‐‐‐‐‐‐‐‐‐‐");

// byte[] getBytes ():把字符串转换为字节数组
byte[] bytes = s.getBytes();
for(int x = 0; x < bytes.length; x++) {
System.out.println(bytes[x]);
}
System.out.println("‐‐‐‐‐‐‐‐‐‐‐");

// 替换字母it为大写IT
String str = "itcast itheima";
String replace = str.replace("it", "IT");
System.out.println(replace); // ITcast ITheima
System.out.println("‐‐‐‐‐‐‐‐‐‐‐");
}
}

CharSequence 是一个接口,也是一种引用类型。作为参数类型,可以把String对象传递到方法中。

分割功能的方法

  • public String[] split(String regex) :将此字符串按照给定的regex(规则)拆分为字符串数组。

示例:

1
2
3
4
5
6
7
8
9
10
public class String_Demo03 {
public static void main(String[] args) {
//创建字符串对象
String s = "aa|bb|cc";
String[] strArray = s.split("|"); // ["aa","bb","cc"]
for(int x = 0; x < strArray.length; x++) {
System.out.println(strArray[x]); // aa bb cc
}
}
}

static关键字


概述

关于 static 关键字的使用,它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属于某个对象的。也就是说,既然属于类,就可以不靠创建对象来调用了。

定义和使用格式

类变量

static 修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。

  • 类变量:使用 static 关键字修饰的成员变量。

定义格式:

1
static 数据类型 变量名;

静态方法

static 修饰成员方法时,该方法称为 类方法 。静态方法在声明中有 static ,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。

  • 类方法:使用 static 关键字修饰的成员方法,习惯称为静态方法

定义格式:

1
2
3
修饰符 static 返回值类型 方法名 (参数列表){
// 执行语句
}
  • 静态方法调用的注意事项
    • 静态方法可以直接访问类变量和静态方法。
    • 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
    • 静态方法中,不能使用this关键字。

小贴士:静态方法只能访问静态成员。

调用格式

static 修饰的成员可以并且建议通过类名直接访问,虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用一个静态成员,但是不建议,会出现警告信息。

格式:

1
2
3
4
5
// 访问类变量
类名.类变量名;

// 调用静态方法
类名.静态方法名(参数);

静态原理图解

static 修饰的内容:

  • 是随着类的加载而加载的,且只加载一次。
  • 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
  • 它优先于对象存在,所以,可以被所有对象共享。

静态代码块

  • 静态代码块:定义在成员位置,使用 static 修饰的代码块{ }。
    • 位置:类中方法外。
    • 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。

格式:

1
2
3
4
5
public class ClassName{
static {
// 执行语句
}
}

作用:给类变量进行初始化赋值。用法演示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Game {
public static int number;
public static ArrayList<String> list;
static {
// 给类变量赋值
number = 2;
list = new ArrayList<String>();
// 添加元素到集合中
list.add("张三");
list.add("李四");
}
}

小贴士:

static 关键字,可以修饰变量、方法和代码块。在使用的过程中,其主要目的还是想在不创建对象的情况
下,去调用方法。下面将介绍两个工具类,来体现static 方法的便利。

Arrays类


概述

java.util.Arrays 此类包含用来操作数组的各种方法,比如排序和搜索等。其所有方法均为静态方法,调用起来非常简单。

操作数组的方法

  • public static String toString(int[] a) :返回指定数组内容的字符串表示形式.
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
// 定义int 数组
int[] arr = {2,34,35,4,657,8,69,9};
// 打印数组,输出地址值
System.out.println(arr); // [I@2ac1fdc4

// 数组内容转为字符串
String s = Arrays.toString(arr);
// 打印字符串,输出内容
System.out.println(s); // [2, 34, 35, 4, 657, 8, 69, 9]
}
  • public static void sort(int[] a) :对指定的 int 型数组按数字升序进行排序。
1
2
3
4
5
6
7
8
public static void main(String[] args) {
// 定义int 数组
int[] arr = {24, 7, 5, 48, 4, 46, 35, 11, 6, 2};
System.out.println("排序前:"+ Arrays.toString(arr)); // 排序前:[24, 7, 5, 48, 4, 46, 35, 11, 6,2]
// 升序排序
Arrays.sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));// 排序后:[2, 4, 5, 6, 7, 11, 24, 35, 46,48]
}

Math类


概述

java.lang.Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。

基本运算的方法

  • public static double abs(double a) :返回 double 值的绝对值。
1
2
double d1 = Math.abs(‐5); //d1的值为5
double d2 = Math.abs(5); //d2的值为5
  • public static double ceil(double a) :返回大于等于参数的最小的整数。
1
2
3
double d1 = Math.ceil(3.3); //d1的值为 4.0
double d2 = Math.ceil(‐3.3); //d2的值为 ‐3.0
double d3 = Math.ceil(5.1); //d3的值为 6.0
  • public static double floor(double a) :返回小于等于参数最大的整数。
1
2
3
double d1 = Math.floor(3.3); //d1的值为3.0
double d2 = Math.floor(‐3.3); //d2的值为‐4.0
double d3 = Math.floor(5.1); //d3的值为 5.0
  • public static long round(double a) :返回最接近参数的 long。(相当于四舍五入方法)
1
2
long d1 = Math.round(5.5); //d1的值为6.0
long d2 = Math.round(5.4); //d2的值为5.0

继承、super、this、抽象类

继承


概述

由来

多个类中存在相同属性和行为时,将这些内容抽取到单独的一个类中,那么多个类无需再定义这些属性和行为,只要继承一个类即可。

其中,多个类可以称为子类,单独那一个类称为父类超类(superclass)或者基类

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

定义

  • 继承:就是子类继承父类的属性行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为

好处

  1. 提高代码的复用性
  2. 类与类之间产生了关系,是多态的前提

继承的格式

通过 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 定义员工类Employee,做为父类
*/
class Employee {
String name; // 定义name属性
// 定义员工的工作方法
public void work() {
System.out.println("尽心尽力地工作");
}
}

/*
* 定义讲师类Teacher 继承 员工类Employee
*/
class Teacher extends Employee {
// 定义一个打印name的方法
public void printName() {
System.out.println("name=" + name);
}
}

/*
* 定义测试类
*/
public class ExtendDemo01 {
public static void main(String[] args) {
// 创建一个讲师类对象
Teacher t = new Teacher();

// 为该员工类的name属性进行赋值
t.name = "小明";

// 调用该员工的printName()方法
t.printName(); // name = 小明

// 调用Teacher类继承来的work()方法
t.work(); // 尽心尽力地工作
}
}

继承后的特点——成员变量

成员变量不重名

如果子类父类中出现不重名的成员变量,这时的访问是没有影响的

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
class Fu {
// Fu中的成员变量。
int num = 5;
}

class Zi extends Fu {
// Zi中的成员变量
int num2 = 6;
// Zi中的成员方法
public void show() {
// 访问父类中的num,
System.out.println("Fu num="+num); // 继承而来,所以直接访问。
// 访问子类中的num2
System.out.println("Zi num2="+num2);
}
}

class ExtendDemo02 {
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}

演示结果:
Fu num = 5
Zi num2 = 6

成员变量重名

如果子类父类出现重名的成员变量,这时的访问是有影响的

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
class Fu {
// Fu中的成员变量。
int num = 5;
}

class Zi extends Fu {
// Zi中的成员变量
int num = 6;
public void show() {
// 访问父类中的num
System.out.println("Fu num=" + num);
// 访问子类中的num
System.out.println("Zi num=" + num);
}
}

class ExtendsDemo03 {
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}

演示结果:
Fu num = 6
Zi num = 6

子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用 super 关键字,修饰父类成员变量,类似于之前学过的 this

使用格式:

1
super.父类成员变量名

子类方法需要修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Zi extends Fu {
// Zi中的成员变量
int num = 6;
public void show() {
//访问父类中的num
System.out.println("Fu num=" + super.num);
//访问子类中的num
System.out.println("Zi num=" + this.num);
}
}

演示结果:
Fu num = 5
Zi num = 6

小贴士:Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu 类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。

继承后的特点——成员方法

成员方法不重名

如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Fu{
public void show(){
System.out.println("Fu类中的show方法执行");
}
}

class Zi extends Fu{
public void show2(){
System.out.println("Zi类中的show2方法执行");
}
}

public class ExtendsDemo04{
public static void main(String[] args) {
Zi z = new Zi();
//子类中没有show方法,但是可以找到父类方法去执行
z.show();
z.show2();
}
}

成员方法重名——重写(Override)

如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)。

  • 方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Fu {
public void show() {
System.out.println("Fu show");
}
}

class Zi extends Fu {
//子类重写了父类的show方法
public void show() {
System.out.println("Zi show");
}
}

public class ExtendsDemo05{
public static void main(String[] args) {
Zi z = new Zi();
// 子类中有show方法,只执行重写后的show方法
z.show(); // Zi show
}
}

注意事项

  1. 类方法覆盖父类方法,必须要保证权限大于等于父类权限。
  2. 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。

继承后的特点——构造方法

当类之间产生了关系,其中各类中的构造方法,又产生了哪些影响呢?

首先我们要回忆两个事情,构造方法的定义格式和作用。

  1. 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
  2. 构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个 super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Fu {
private int n;
Fu(){
System.out.println("Fu()");
}
}

class Zi extends Fu {
Zi(){
// super(),调用父类构造方法
super();
System.out.println("Zi()");
}
}

public class ExtendsDemo07{
public static void main (String args[]){
Zi zi = new Zi();
}
}

输出结果:
Fu()
Zi()

super和this


父类空间优先于子类对象产生

在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。

super和this的含义

  • super :代表父类的存储空间标识(可以理解为父亲的引用)。
  • this :代表当前对象的引用(谁调用就代表谁)。

super和this的用法

  1. 访问成员

    1
    2
    3
    4
    5
    this.成员变量 // 本类的
    super.成员变量 // 父类的

    this.成员方法名() // 本类的
    super.成员方法名() // 父类的
  2. 访问构造方法

    1
    2
    this(...) // 本类的构造方法
    super(...) // 父类的构造方法

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

继承的特点

  1. Java只支持单继承,不支持多继承。

    1
    2
    3
    //一个类只能有一个父类,不可以有多个父类。
    class C extends A{} //ok
    class C extends A,B... //error
  2. Java支持多层继承(继承体系)。

    1
    2
    3
    class A{}
    class B extends A{}
    class C extends B{}

    顶层父类是Object类。所有的类默认继承Object,作为父类。

  3. 子类和父类是一种相对的概念。

抽象类


概述

由来

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类

定义

  • 抽象方法 : 没有方法体的方法。
  • 抽象类:包含抽象方法的类。

abstract使用格式

抽象方法

使用 abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

1
修饰符 abstract 返回值类型 方法名 (参数列表);

抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。

定义格式:

1
2
3
abstract class 类名字 {

}

抽象的使用

继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

注意事项

关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

接口、多态

接口


概述

接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9)。

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,接口。

接口的使用,它不能创建对象,但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。

定义格式

1
2
3
4
5
6
public interface 接口名称 {
//抽象方法
//默认方法
//静态方法
//私有方法
}

含有抽象方法

抽象方法:使用 abstract 关键字修饰,可以省略,没有方法体。该方法供子类实现使用。

1
2
3
public interface InterFaceName {
public abstract void method();
}

含有默认方法和静态方法

默认方法:使用 default 修饰,不可省略,供子类调用或者重写。

静态方法:使用 static 修饰,供接口直接调用。

1
2
3
4
5
6
7
8
public interface InterFaceName {
public default void method() {
//执行语句
}
public static void method2() {
//执行语句
}
}

含有私有方法和私有静态方法

私有方法:使用 private 修饰,供接口中的默认方法或者静态方法调用。

1
2
3
4
5
public interface InterFaceName {
private void method() {
//执行语句
}
}

基本的实现

实现的概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。

非抽象子类实现接口:

  1. 必须重写接口中所有抽象方法。
  2. 继承了接口的默认方法,即可以直接调用,也可以重写。
1
2
3
4
class 类名 implements 接口名 {
//重写接口中抽象方法【必须】
//重写接口中默认方法【可选】
}

抽象方法的使用

必须全部实现

定义接口:

1
2
3
4
5
public interface LiveAble {
//定义抽象方法
public abstract void eat();
public abstract void sleep();
}

定义实现类:

1
2
3
4
5
6
7
8
9
10
11
public class Animal implements LiveAble {
@Override
public void eat() {
System.out.println("吃东西");
}

@Override
public void sleep() {
System.out.println("晚上睡");
}
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
public class InterfaceDemo {
public static void main(String[] args) {
//创建子类对象
Animal a = new Animal();
//调用实现后的方法
a.eat();
a.sleep();
}
}
输出结果:
吃东西
晚上睡

默认方法的使用

可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。

  1. 继承默认方法

    定义接口:

    1
    2
    3
    4
    5
    public interface LiveAble {
    public default void fly(){
    System.out.println("天上飞");
    }
    }

    定义实现类:

    1
    2
    3
    public class Animal implements LiveAble {
    //继承,什么都不用写,直接调用
    }

    定义测试类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class InterfaceDemo {
    public static void main(String[] args) {
    //创建子类对象
    Animal a = new Animal();
    //调用默认方法
    a.fly();
    }
    }
    输出结果:
    天上飞
  2. 重写默认方法

    定义接口:

    1
    2
    3
    4
    5
    public interface LiveAble {
    public default void fly(){
    System.out.println("天上飞");
    }
    }

    定义实现类:

    1
    2
    3
    4
    5
    6
    public class Animal implements LiveAble {
    @Override
    public void fly() {
    System.out.println("自由自在的飞");
    }
    }

    定义测试类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class InterfaceDemo {
    public static void main(String[] args) {
    //创建子类对象
    Animal a = new Animal();
    //调用重写方法
    a.fly();
    }
    }
    输出结果:
    自由自在的飞

静态方法的使用

静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用

定义接口:

1
2
3
4
5
public interface LiveAble {
public static void run(){
System.out.println("跑起来~~~");
}
}

定义实现类:

1
2
3
public class Animal implements LiveAble {
//无法重写静态方法
}

定义测试类:

1
2
3
4
5
6
7
8
public class InterFaceDemo {
public static void main(String[] args) {
//Animal.run(); //【错误】无法继承方法,也无法调用
LivaAble.run();
}
}
输出结果:
跑起来~~~

私有方法的使用

  • 私有方法:只有默认方法可以调用
  • 私有静态方法:默认方法和静态方法可以调用

如果一个接口中有多个默认方法,并且方法中有重复的内容,那么可以抽取出来,封装到私有方法中,供默认方法去调用。从设计的角度讲,私有的方法是对默认方法静态方法的辅助。同学们在已学技术的基础上,可以自行测试。

定义接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface LiveAble {
default void func(){
func1();
func2();
}

private void func1(){
System.out.println("跑起来~~~");
}

private void func2(){
System.out.println("跑起来~~~");
}
}

接口的多实现

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

1
2
3
4
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3...{
//重写接口中抽象方法【必须】
//重写接口中默认方法【不重写时可选】
}

抽象方法

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名,只需要重写一次

定义多个接口:

1
2
3
4
5
6
7
8
interface A {
public abstract void showA();
public abstract void show();
}
interface B {
public abstract void showB();
public abstract void show();
}

定义实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class C implements A,B {
@Override
public void showA() {
System.out.println("showA");
}

@Override
public void showB() {
System.out.println("showB");
}

@Override
public void show() {
System.out.println("show");
}
}

默认方法

接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次

定义多个接口:

1
2
3
4
5
6
7
8
interface A {
public default void methodA(){}
public default void method(){}
}
interface B {
public default void methodB(){}
public default void method(){}
}

定义实现类:

1
2
3
4
5
6
public class C implements A,B {
@Override
public void method() {
System.out.println("method");
}
}

静态方法

接口中,存在同名的静态方法并不会冲突,原因只能通过各自接口名访问静态方法。

优先级的问题

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。

1
2
3
4
5
interface A {
public default void methodA(){
System.out.println("AAAAAAAAAAA");
}
}

定义父类:

1
2
3
4
5
class D {
public void methodA(){
System.out.println("DDDDDDDDDDD");
}
}

定义子类:

1
2
3
class C extends D implements A {
//未重写methodA方法
}

定义测试类:

1
2
3
4
5
6
7
8
public class Test() {
public static void main(Strong[] args){
C c = new C();
c.methodA();
}
}
输出结果:
DDDDDDDDDDD

接口的多继承【了解】

一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends关键字,子接口继承父接口的方法。如果父接口中的默认方法又重名的,那么子接口需要重写一次

定义父接口:

1
2
3
4
5
6
7
8
9
10
interface A {
public default void method(){
System.out.println("AAAAAAAAAAAAAAAAAAA");
}
}
interface B {
public default void method(){
System.out.println("BBBBBBBBBBBBBBBBBBB");
}
}

定义子接口:

1
2
3
4
5
6
interface D extends A,B {
@override
public default void method(){
System.out.println("DDDDDDDDDDDDDD");
}
}

小贴士:

子接口重写默认方法时,default关键字可以保留。

子类重写默认方法时,default关键字不可以保留

其他成员特点

  • 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用 public static final 修饰。
  • 接口中,没有构造方法,不能创建对象。
  • 接口中,没有静态代码块。

多态


概述

引入

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

生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。在比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,是可以体现出来的不同的状态。多态,描述的就是这样的状态。

定义

  • 多态:是指同一行为没具有多个不同表现形式。

前提【重点】

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】

多态的体现

多态的体现的格式:

1
2
父类类型 变量名 = new 子类对象;
变量名方法名();

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

1
2
Fu f = new Zi();
f.method();

当使用多态调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的时子类重写后方法。

定义父类:

1
2
3
public abstract class Animal {
public abstract void eat();
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
class Cat extends Animal {
public void eat(){
System.out.println("吃鱼");
}
}

class Dog extends Animal {
public void eat(){
System.out.println("吃骨头")
}
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public static void main(String[] args){
//多态形式,创建对象
Animal a1 = new Cat();
//调用的时 Cat 的 eat
a1.eat();

//多态形式,创建对象
Animal a2 = new Dog();
//调用的时 Dog 的 eat
a2.eat();
}
}

多态的好处

实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。

定义父类:

1
2
3
public abstract class Animal{
public abstract void eat();
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
class Cat extends Animal{
public void eat(){
System.out.println("吃鱼");
}
}

class Dog extends Animal{
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
25
26
27
28
29
30
public class Test{
public static void main(String[] args){
//多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();

//调用showCatEat
showCatEat(c);
//调用showDogEat
showDogEat(d);

/*
以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
而执行效果一致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat(Cat c){
c.eat();
}

public static void showDogEat(Dog d){
d.eat();
}

public static void showAnimalEat(Animal a){
a.eat();
}
}

由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以吧Cat对象和Dog对象,传递给方法。

当然eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上俩种方法。

不仅仅是代替,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。

所以、多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。

引用类型转换

多态的转换分为向上转型与向下转型俩种:

向上转型

  • 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。

当父类引用指向一个子类对象时,便是向上转型。

1
2
父类类型 变量名 = new 子类类型();
Animal a = new Cat();

向下转型

  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

1
2
子类类型 变量名 = (子类类型)父类变量名;
Cat c = (Cat) a;

为什么要转型

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点“小麻烦”。所以,想要调用子类特有的方法必须做向下转型。

定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Animal {
abstract void eat();
}

class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}

class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {
System.out.println("看家");
}
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
a.eat(); //调用的是 Cat 的 eat

//向下转型
Cat c = (Cat)a;
c.catchMouse(); //调用的是 Cat 的 catchMouse
}
}

转型的异常

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
a.eat(); //调用的是 Cat 的 eat

//向下转型
Dog d = (Dog)a;
d.watchHouse(); //调用的是 Dog 的 watchHouse 【运行报错】
}
}

这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了
Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验.

1
2
3
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true
如果变量不属于该数据类型,返回false

所以,转换前,我们最好做一个判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
a.eat(); //调用的是 Cat 的 eat

//向下转型
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); //调用的是 Cat 的 catchMouse
} else if(a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); //调用的是 Dog 的 watchHouse
}
}
}

final、权限、内部类

第一章 final关键字


概述

学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供 final 关键字,用于修饰不可改写内容。

  • final:不可改变。可以用于修饰类、方法和变量。
    • 类:被修饰的类,不能被继承。
    • 方法:被修饰的方法,不能被重写。
    • 变量:被修饰的变量,不能被重新赋值。

使用方式

修饰类

1
2
3
final class 类名 {

}

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。

修饰方法

1
2
3
修饰符 final 返回值类型 方法名(参数列表){
//方法体
}

重写被 final 修饰的方法,编译时就会报错。

修饰变量

  1. 局部变量——基本类型

    基本类型的局部变量,被 final 修饰后,只能赋值一次,不能再更改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class FinalDemo1 {
    public static void main(String[] args) {
    //声明变量,使用final修饰
    final int a;
    //第一次赋值
    a = 10;
    //第二次赋值
    a = 20;//报错,不可重新赋值


    //声明变量,直接赋值,使用final修饰
    final int b = 10;
    //第二次赋值
    b = 20;//报错,不可重新赋值
    }
    }
  2. 局部变量——引用类型

    引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class FinalDemo2 {
    public static void main(String[] args) {
    //创建User对象
    final User u = new User();
    //创建另一个user对象
    u = new User();//报错,指向了新的对象,地址值改变

    //调用setName方法
    u.setName("张三");//可以修改
    }
    }
  3. 成员变量

    成员变量涉及到初始化的问题,初始化方法有两种,只能二选一:

    • 显示初始化

      1
      2
      3
      4
      public class User {
      final String USERNAME = "张三";
      private int age;
      }
    • 构造方法初始化

      1
      2
      3
      4
      5
      6
      7
      8
      public class User {
      final String USERNAME;
      private int age;
      public User(String username,int age){
      this.USERNAME = username;
      this.age = age;
      }
      }

被final修饰的常量名称,一般都有书写格式,所有字母都大写

权限修饰符


概述

在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限

  • public:公共的
  • protected:受保护的
  • default:默认的
  • private:私有的

不同权限的访问能力

public protected default(空的) private
同一类中
同一包中(子类与无关类)
不同包的子类
不同包中的无关类

可见,public具有最大权限。private则时最小权限。

编写代码时,如果没有特殊的 考虑,建议这样使用权限:

  • 成员变量使用 private ,隐藏细节。
  • 构造方法使用 public ,方便创建对象。
  • 成员方法使用 public ,方便调用方法。

小贴士:不加权限修饰符,其访问能力与default修饰符相同

内部类


概述

什么时内部类

将一个类A定义在类一个类B里面,里面的那个类就称为内部类,B则称为外部类

成员内部类

  • 成员内部类:定义在类中方法外的类。
1
2
3
4
5
class 外部类 {
class 内部类 {

}
}

在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这样结构。比如,汽车类 Car 中包含发动机类 Engine ,这时,Engine 就可以使用内部类来描述,定义在成员位置。

1
2
3
4
5
class Car {//外部类
class Engne {//内部类

}
}

访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。

创建内部类格式:

1
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

访问演示,代码如下:

定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {
private boolean live = true;
class Heart {
public void jump() {
//直接访问外部类成员
if(live) {
System.out.println("心脏在跳动");
} else {
System.out.println("心脏不跳了");
}
}
}

public Boolean isLivae() {
return live;
}

public void setLive(boolean live) {
this.live = live;
}
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InnerDemo {
public static void main(String[] args) {
//创建外部类对象
Person p = new Person();
//创建内部类对象
Heart heart = p.new.Heart();

//调用内部类方法
heart.jump();
//调用外部类方法
p.setLive(false);
//调用内部类方法
heart.jump();
}
}
输出结果:
心脏在跳动
心脏不跳了

内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。

比如,Person$Heart.class

匿名内部类【重点】

  • 匿名内部类:是内部类的简化写法。它的本质是一个 带具体实现的 父类或者父接口的 匿名的 子类对象

开发中,最常用到的内部类就是匿名内部类了,以接口举例,当你使用一个接口时,视乎得做如下几步操作,

  1. 定义子类
  2. 重写接口中的方法
  3. 创建子类对象
  4. 调用重写后的方法

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成异步呢?匿名内部类就是做这样的快捷方式。

前提

匿名内部类必须继承一个父类或者实现一个父接口

格式

1
2
3
4
5
6
7
new 父类名或者接口名(){
//方法重写
@Override
public void method() {
//执行语句
}
};

使用方式

以接口为例,匿名内部类的使用。

定义接口:

1
2
3
public interface FlyAble {
public abstract void fly();
}

创建匿名内部类,并调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InnerDemo1 {
public static void main(String[] args) {
/*
1.等号右边:是匿名内部类,定义并创建该接口的子类对象
2.等号左边:是多态赋值,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble() {
@Override
public void fly() {
System.out.println("我飞了~~~");
}
};

//调用 fly方法,执行重写后的方法
f.fly();
}
}

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InnerDemo2 {
public static void main(String[] args) {
/*
1.等号右边:定义并创建该接口的子类对象
2.等号左边:是多态,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble() {
@Override
public void fly() {
System.out.println("我飞了~~~");
}
};

//将f传递给showFly方法中
showFly(f);
}
public static void showFly(FlyAble f) {
f.fly();
}
}

以上两步,也可以简化为一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InnerDemo3 {
public static void main(String[] args) {
/*
创建匿名内部类,直接传递给showFly(FlyAble f)
*/
showFly(new FlyAble() {
@Override
public void fly() {
System.out.println("我飞了~~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}

引用类型用法总结


实际的开发中,引用类型的使用非常重要,也是非常普遍的。我们可以在理解基本类型的使用方式基础上,进一步去掌握引用类型的使用方式。基本类型可以作为变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。

class作为成员变量

再定义一个类Role(游戏角色)时

1
2
3
4
5
class Role {
int id;//角色id
int blood;//生命值
String name;//角色名称
}

使用 int 类型表示角色id和生命值,使用 String 类型表示姓名。此时,String 本身就是引用类型,由于使用的方式类似常量,所以往往忽略了它是引用类型的存在。如果我们继续丰富这个类的定义,给 Role 增加武器,穿戴装备等属性,我们将如何编写呢?

定义武器类,将增加攻击能力:

1
2
3
4
class weapon {
String name;//武器名称
int hurt;//伤害值
}

定义穿戴盔甲类,将增加防御能力,也就是提升生命值:

1
2
3
4
class Armour {
String name;//装备名称
int protect;//防御值
}

定义角色类:

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 Role {
int id;
int blood;
String name;
//添加武器属性
Weapon wp;
//添加盔甲属性
Armour ar;

//提供get/set方法
public Weapon getWp() {
return wp;
}
public void setWeapon(Weapon wp) {
this.wp = wp;
}
public Armour getArmour() {
return ar;
}
public void setArmour(Armour ar) {
this.ar = ar;
}

//攻击方法
public void attack(){
System.out.println("使用"+ wp.getName() +", 造成"+wp.getHurt()+"点伤害");
}

//穿戴盔甲
public void wear(){
// 增加防御,就是增加blood值
this.blood += ar.getProtect();
System.out.println("穿上"+ar.getName()+", 生命值增加"+ar.getProtect());
}
}

测试类:

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 Test {
public static void main(String[] args) {
// 创建Weapon 对象
Weapon wp = new Weapon("屠龙刀" , 999999);
// 创建Armour 对象
Armour ar = new Armour("麒麟甲",10000);
// 创建Role 对象
Role r = new Role();

// 设置武器属性
r.setWeapon(wp);

// 设置盔甲属性
r.setArmour(ar);

// 攻击
r.attack();
// 穿戴盔甲
r.wear();
}
}
输出结果:
使用屠龙刀,造成999999点伤害
穿上麒麟甲 ,生命值增加10000

类作为成员变量时,对它进行赋值的操作,实际上,是赋给它该类的一个对象。

interface作为成员变量

接口是对方法的封装,对应游戏当中,可以看作是扩展游戏角色的技能。所以,如果想扩展更强大技能,我们在
Role 中,可以增加接口作为成员变量,来设置不同的技能。

定义接口:

1
2
3
4
//法术攻击
public interface FaShuShill {
public abstract void faShuAttack();
}

定义角色类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Role {
FaShuSkill fs;
public void setFaShuSkill(FaShuSkill fs) {
this.fs = fs;
}

//法术攻击
public void faShuSkillAttack(){
System.out.print("发动法术攻击:");
fs.faShuAttack();
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
public class Test {
public static void main(String[] args) {
// 创建游戏角色
Role role = new Role();
// 设置角色法术技能
role.setFaShuSkill(new FaShuSkill() {

@Override
public void faShuAttack() {
System.out.println("纵横天下");
}
});
// 发动法术攻击
role.faShuSkillAttack();
// 更换技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void faShuAttack() {
System.out.println("逆转乾坤");
}
});
// 发动法术攻击
role.faShuSkillAttack();
}
}
输出结果:
发动法术攻击:纵横天下
攻击完毕

发动法术攻击:逆转乾坤
攻击完毕

我们使用一个接口,作为成员变量,以便随时更换技能,这样的设计更为灵活,增强了程序的扩展性。

接口作为成员变量时,对它进行赋值的操作,实际上,是赋给它该接口的一个子类对象。

interface作为方法参数和返回值类型

当接口作为方法的参数时,需要传递什么呢?当接口作为方法的返回值类型时,需要返回什么呢?对,其实都是它的子类对象。 ArrayList 类我们并不陌生,查看API我们发现,实际上,它是 java.util.List 接口的实现类。所以,当我们看见 List 接口作为参数或者返回值类型时,当然可以将 ArrayList 的对象进行传递或返回。

请观察如下方法:获取某集合中所有的偶数

定义方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static List<Integer> getEvenNum(List<Integer> list) {
//创建保存偶数的集合
ArrayList<Integer> evenList = new ArrayList<>();
//遍历集合list,判断元素为偶数,就添加到evenList中
for (int i = 0; i < list.size(); i++) {
Integer integer = list.get(i);
if (integer % 2 == 0) {
evenList.add(integer);
}
}
/*
返回偶数集合
因为getEvenNum
因为getEvenNum方法的返回值类型是List,而ArrayList是List的子类,
所以evenList可以返回
*/
return evenList;
}

调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
// 创建ArrayList集合,并添加数字
ArrayList<Integer> srcList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
srcList.add(i);
}

/*
获取偶数集合
因为getEvenNum方法的参数是List,而ArrayList是List的子类,
所以srcList可以传递
*/
List list = getEvenNum(srcList);
System.out.println(list);
}
}

接口作为参数时,传递它的子类对象。

接口作为返回值类型时,返回它的子类对象。

常用类

Object类


概述

java.lang.Object

Object 是类层次结构的根(最顶层)类。每个类都使用 Object 作为超(父)类。所有对象(包括数组)都实现这个类的方法。

1
2
3
public class MyClass /*extends Object*/ {
// ...
}
  • public String toString():返回该对象的字符串表示。
  • public boolean equals(Object obj):指示其他某个对象是否与此对象“相等”。

toString方法

方法摘要

  • public String toString():返回该对象的字符串表示。

toString方法返回该对象的字符串表示,其实该字符串内容就是对象的类型+@+内存地址值。

由于toString方法返回的结果是内存地址,而在开发中,经常需要按照对象的属性得到相应的字符串表现形式,因此也需要重写它。

覆盖重写

如果不希望使用toString方法的默认行为,则可以对它进行覆盖重写。例如自定义的Person类:

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

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}

// 省略构造器与Getter Setter
}

在IntelliJ IDEA中,可以点击Code菜单中的 Generate...,也可以使用快捷键 alt+insert,点击 toString()选项。选择需要包含的成员变量并确定.

小贴士: 在我们直接使用输出语句输出对象名的时候,其实通过该对象调用了其toString()方法。

equals方法

方法摘要

  • public boolean equals(Object obj):指示其他某个对象是否与此对象“相等”。

调用成员方法equals并指定参数为另一个对象,则可以判断这两个对象是否是相同的。这里的“相同”有默认和自定义两种方式。

默认地址比较

如果没有覆盖重写equals方法,那么Object类中默认进行==运算符的对象地址比较,只要不是同一个对象,结果必然为false。

对象内容比较

如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法。

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

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

@Override
public boolean equals(Object o) {
// 如果对象地址一样,则认为相同
if (this == o)
return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (o == null || getClass() != o.getClass())
return false;
// 转换为当前类型
Person person = (Person) o;
// 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果
return age == person.age && Objects.equals(name, person.name);
}
}

这段代码充分考虑了对象为空、类型一致等问题,但方法内容并不唯一。大多数IDE都可以自动生成equals方法的代码内容。在IntelliJ IDEA中,可以使用 Code 菜单中的 Generate… 选项,也可以使用快捷键 alt+insert ,并选择 equals() and hashCode() 进行自动代码生成。

Objects类

在刚才IDEA自动重写equals代码中,使用到了java.util.Objects类,那么这个类是什么呢?

JDK7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。

在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:

  • public static boolean equals(Object a, Object b):判断两个对象是否相等。

我们可以查看一下源码,学习一下:

1
2
3
public static boolean equals(Object a, Object b) {  
return (a == b) || (a != null && a.equals(b));
}

日期时间类


Date类

概述

java.util.Date 类 表示特定的瞬间,精确到毫秒。

  • public Date() :分配Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。获取的就是当前系统的日期和时间
  • public Date(long date) : 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。传递毫秒值,把毫秒值转换为Date日期

tips:由于中国处于东八区,所以我们的基准时间为1970年1月1日8时0分0秒。

1
2
3
4
5
6
7
8
import java.util.Date;

public class Demo01Date {
//创建日期对象,获取当前系统时间
System.out.println(new Date());
//创建日期对象,把传递的毫秒值转换成日期对象
System.out.println(new Date(0L));
}

tips:在使用println方法时,会自动调用Date类中的toString方法。Date类对Object类中的toString方法进行了覆盖重写,所以结果为指定格式的字符串。

常用方法

Date类中的多数方法已经过时,常用的方法有:

  • public long getTime() :把时间对象转换成对应的时间毫秒值(相当于System.currentTimeMillis())。

DateFormat类

java.text.DateFormat 是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本直之间的转换,也就是可以在Date对象与String对象之间进行来回转换。

  • 格式化:按照指定的格式,从Date对象转换为String对象
  • 解析:按照指定的格式,从String对象转换为Date对象。

构造方法

由于DateFormat为抽象类,不能直接使用,所以需要常用的子类 java.text.SimpleDateFormat .这个类需要以一个模式(格式)来指定格式化或解析的标准。

  • Public SimpleDateFormat(String pattern):用给定的模式和默认语音环境的日期格式符号构造SimpleDateFormat。

参数pattern是一个字符串,代表日期时间的自定义格式。

格式规则

常用的格式规则为:

标识字母(区分大小写) 含义
y
M
d
H
m
s

备注:更详细的格式规则,可以参考SimpleDateFormat类的API文档0。

创建SimpleDateFormat对象的代码如:

1
2
3
4
5
6
7
8
9
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class Demo02SimpleDateFormat {
public static void main(String[] args) {
//对应的日期格式如:2018-01-16 15:06:38
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}

常用方法

DateFormat类的常用方法有:

  • public String format(Date date):将Date对象格式化为字符串。
  • Public Date parse(String source):将字符串解析为Date对象。
format方法

使用format方法的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
把Date对象转换成String
*/
public class Demo03DateFormatMethod {
public static void main(String[] args) {
Date date = new Date();
//创建日期格式化对象,在获取格式化对象时可以指定风格
DateFormat df = new SimpleDateFormat("yyyy年MM月dd日");
String str = df.format(date);
System.out.println(str);
}
}
parse方法

使用parse方法的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo02SimpleDateFormat {
public static void main(String[] args) throws ParseException {
DateFormat df = new SimpleDateFormat("yyyy年MM月dd日");
String str = "2021年08月23日";
Date date = df.parse(str);
System.out.println(date);//Mon Aug 23 00:00:00 CST 2021
}
}

注意:parse方法声明了一个异常叫做ParseException解析异常,如果字符串和构造方法中的模式不一样,那么程序就会抛出此异常,调用一个抛出了异常的方法,就必须处理这个异常,要么throws继续声明抛出了一个异常,要么try…catch自己处理这个异常

Calendar类

概述

java.util.Calendar是日历类,在Date后出现,替换掉了许多Date的方法。该类将所有可能用到的时间信息封装为静态成员变量,方便获取。日历类就是方便获取各个时间属性的。

Calendar类是一个抽象类,里面提供了很多操作日历字段的方法(YEAR、MONTH、DAY_OF、HOUR)。

获取方式

Calendar类无法直接创建对象使用,里边有一个静态方法叫 getInstance() ,该烦烦烦返回了Calendar类的子类对象。

  • public static Calendar getInstance() :使用默认时区和语言环境获得一个日历
1
2
3
4
5
6
7
import java.util.Calendar;

public class Demo06CalendarInit {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
}
}

常用方法

  • public int get(int field): 返回给定日历字段的值。
  • public void set(int field,int value): 将给定的日历字段设置为给定值。
  • public abstract void add(int field,int amount): 根据日历的规则,为给定的日历字段添加或减去指定的时间量。
  • public Date getTime(): 返回一个表示此Calendar时间值(从历元到现在的毫秒偏移值)的Date对象。

Calendar类中提供很多成员常量,代表给定的日历字段:

字段值 含义
YEAR
MONTH 月(从0开始,可以+1使用)
DAY_OF_MONTH 月中的天(几号)
HOUR 时(12小时制)
HOUR_OF_DAY 时(24小时制)
MINUTE
SECOND
DAY_OF_WEEK 周中的天(周几,周日为1,可以-1使用)

小贴士:

​ 西方星期的开始为周日,中国为周一。

​ 在Calendar类中,月份的表示是以0-11代表1-12月。

​ 日期是有大小关系的,时间靠后,时间越大。

System类

java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有:

  • public static long currentTimeMillis():返回以毫秒为单位的当前时间。

  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中。

    数组的拷贝动作是系统级的,性能很高。System.arraycopy方法具有5个参数,含义分别为:

参数序号 参数名称 参数类型 参数含义
1 src Object 源数组
2 srcPos int 源数组索引起始位置
3 dest Object 目标数组
4 destPos int 目标数组索引起始位置
5 length int 复制元素个数

StringBulider类

概述

StringBuilder又称为可变字符序列,它是一个类似于 String 的字符串缓冲区,通过某些方法调用可以改变该序列的长度和内容。

原来StringBuilder是个字符串的缓冲区,即它是一个容器,容器中可以装很多字符串。并且能够对其中的字符串进行各种操作。

它的内部拥有一个数组用来存放字符串内容,进行字符串拼接时,直接在数组中加入新内容。StringBuilder会自动维护数组的扩容。原理如下图所示:(默认16字符空间,超过自动扩充)

构造方法

  • public StringBuilder():构造一个空的StringBuilder容器。
  • public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去。

常用方法

  • public StringBuilder append(...):添加任意类型数据的字符串形式,并返回当前对象自身。

    append方法具有多种重载形式,可以接收任意类型的参数。

  • public StringBuilder reverse():将此字符序列用其反转形式取代。

  • public String toString():将当前StringBuilder对象转换为String对象。

包装类

概述

Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

装箱与拆箱

基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“:

  • 装箱:从基本类型转换为对应的包装类对象。
  • 拆箱:从包装类对象转换为对应的基本类型。

用Integer与 int为例:

基本数值—->包装对象

1
2
Integer in1 = new Integer(1);//使用构造函数
Integer in2 = Integer.valueof(4);//使用包装类中的valueof方法

包装对象—->基本数值

1
int num = i.intValue();

自动装箱和自动拆箱

由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

1
2
3
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

基本类型与字符串之间的转换

基本类型转换为String

基本类型转换String总共有三种方式

  • 基本类型直接与””相连接即可;如:34+””(最常用)
  • 使用包装类中的静态方法 public static String toString(int i):返回一个表示指定整数的 String 对象
  • 使用String类中的静态方法 public static String valueOf()

String转换成对应的基本类型

除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:

  • public static byte parseByte(String s):将字符串参数转换为对应的byte基本类型。
  • public static short parseShort(String s):将字符串参数转换为对应的short基本类型。
  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static float parseFloat(String s):将字符串参数转换为对应的float基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。
  • public static boolean parseBoolean(String s):将字符串参数转换为对应的boolean基本类型。

代码使用(仅以Integer类的静态方法parseXxx为例)如:

1
2
3
4
5
public class Demo18WrapperParse {
public static void main(String[] args) {
int num = Integer.parseInt("100");
}
}

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

Collection集合

集合概述

  • 集合:是java中提供的一种容器,可以用来存储多个数据。

集合和数组的区别:

  1. 数组的长度是固定的。集合的长度是可变的。
  2. 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

集合框架

JAVASE提供了满足各种需求的API,在使用这些API前,先了解其继承与接口操作架构,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用。

集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map

  • Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.Listjava.util.Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有java.util.ArrayListjava.util.LinkedListSet接口的主要实现类有java.util.HashSetjava.util.TreeSet

常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

Iterator迭代器

Iterator接口

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。

迭代的概念

  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

常用方法

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。

迭代器的实现原理

增强for

增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。

格式:

1
2
3
for(元素的数据类型  变量 : Collection集合or数组){ 
//写操作代码
}

它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。

tips: 新for循环必须有被遍历的目标。目标只能是Collection或者是数组。新式for仅仅作为遍历操作出现。

泛型

概述

泛型:可以在类或方法中预支地使用未知的类型。

tips:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

创建集合对象,不使用泛型

  • 好处:集合不使用泛型,默认的类型就是Object类型,可以存储任意类型的数据
  • 弊端:不安全,会引发异常

创建集合对象,使用泛型

  • 好处:
    1. 避免了类型转换的麻烦,存储的是什么类型,取出的就是什么类型
    2. 把运行期异常(代码运行之后会抛出的异常),提升到了编译期(写代码的时候就会报错)
  • 弊端:泛型是什么类型,就只能存储什么类型的数据

泛型的通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

通配符高级使用—-受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称
  • 意义只能接收该类型及其父类型

数据结构

常见的数据结构

数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:

  • stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。

简单的说:采用该结构的集合,对元素的存取有如下的特点

  • 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。

  • 栈的入口、出口的都是栈的顶端位置。

这里两个名词需要注意:

  • 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
  • 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。

队列

  • 队列queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
  • 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。

数组

  • 数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 查找元素快:通过索引,可以快速访问指定位置的元素

  • 增删元素慢
    • 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。
    • 指定索引位置删除元素:将指定索引位置后的元素全部向前移动一位。

链表

  • 链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。

  • 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素

  • 增删元素快:

    • 增加元素:只需要修改连接下个元素的地址即可。

    • 删除元素:只需要修改连接下个元素的地址即可。

红黑树

  • 二叉树binary tree ,是每个结点不超过2的有序树(tree)

简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。

二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。

我们要说的是二叉树的一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。

红黑树的约束:

  1. 节点可以是红色的或者黑色的

  2. 根节点是黑色的

  3. 叶子节点(特指空节点)是黑色的

  4. 每个红色节点的子节点都是黑色的

  5. 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同

红黑树的特点:

​ 速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍

List集合

概述

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。

List接口特点:

  1. 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  2. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  3. 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

tips:我们在基础班的时候已经学习过List接口的子类java.util.ArrayList类,该类中的方法都是来自List中定义。

常用方法

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index):返回集合中指定位置的元素。
  • public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

List的子类

ArrayList集合

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。

许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。

LinkedList集合

java.util.LinkedList集合数据存储的结构是链表结构。方便元素添加、删除的集合。

常用方法

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public E pop():从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e):将元素推入此列表所表示的堆栈。
  • public boolean isEmpty():如果列表不包含元素,则返回true。

在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。

Set接口

java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。

Set集合有多个子类,这里我们介绍其中的java.util.HashSetjava.util.LinkedHashSet这两个集合。

tips:Set集合取出元素的方式可以采用:迭代器、增强for。

HashSet集合

概述

java.util.HashSetSet接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。java.util.HashSet底层的实现其实是一个java.util.HashMap支持。

HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCodeequals方法。

存储数据的结构(哈希表)

什么是哈希表呢?

JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。

总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

自定义类型元素

给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一

LinkedHashSet集合

在HashSet下面有一个子类java.util.LinkedHashSet,它是链表和哈希表组合的一个数据存储结构。

可变参数

JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化成如下格式:

1
修饰符 返回值类型 方法名(参数类型... 形参名){  }

其实这个书写完全等价与

1
修饰符 返回值类型 方法名(参数类型[] 形参名){  }

只是后面这种定义,在调用时必须传递数组,而前者可以直接传递数据即可。

JDK1.5以后。出现了简化操作。**…** 用在参数上,称之为可变参数。

同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组(这就是简单之处),直接将数组中的元素作为实际参数进行传递,其实编译成的class文件,将这些元素先封装到一个数组中,在进行传递。这些动作都在编译.class文件时,自动完成了。

注意:如果在方法书写时,这个方法拥有多参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置。

Collections

常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。部分方法如下:
  • public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
  • public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

Comparator比较器

public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用java.lang.Comparable接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator接口完成。

那么我们采用的public static <T> void sort(List<T> list)这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能。

public static <T> void sort(List<T> list,Comparator<? super T> )方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:

  • public int compare(String o1, String o2):比较其两个参数的顺序。

    两个对象比较的结果有三种:大于,等于,小于。

    如果要按照升序排序,
    则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)
    如果要按照降序排序
    则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

Comparable和Comparator的区别。

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

Map集合

概述

现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即java.util.Map接口。

我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它们存储数据的形式不同,如下图。

  • Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  • Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  • Collection中的集合称为单列集合,Map中的集合称为双列集合。
  • 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

常用子类

  • **HashMap<K,V>**:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
  • **LinkedHashMap<K,V>**:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。

常用方法

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
  • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
  • boolean containsKey(Object key) 判断集合中是否包含指定的键。
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

集合遍历

键找值方式

键找值方式:即通过元素中的键,获取键所对应的值

步骤:

  1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
  2. 遍历键的Set集合,得到每一个键。
  3. 根据键,获取键所对应的值。方法提示:get(K key)

Entry键值对对象

我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。

既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:

  • public K getKey():获取Entry对象中的键。
  • public V getValue():获取Entry对象中的值。

在Map集合中也提供了获取所有Entry对象的方法:

  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

键值对方式

键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。

步骤:

  1. 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。方法提示:entrySet()

  2. 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。

  3. 通过键值对(Entry)对象,获取Entry对象中的键与值。 方法提示:getkey() getValue()

Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。

存储自定义类型键值

  • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

LinkedHashMap集合

在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。(保证成对元素唯一,并且查询速度很快)

Debug追踪

使用IDEA的断点调试功能,查看程序的运行过程

  1. 在有效代码行,点击行号右边的空白区域,设置断点,程序执行到断点将停止,我们可以手动来运行程序

  2. 点击Debug运行模式

  3. 程序停止在断点上不再执行,而IDEA最下方打开了Debug调试窗口

  4. Debug调试窗口介绍

  5. 快捷键F8,代码向下执行一行,第九行执行完毕,执行到第10行(第10行还未执行)

  6. 切换到控制台面板,控制台显示 请录入一个字符串: 并且等待键盘录入

  7. 快捷键F8,程序继续向后执行,执行键盘录入操作,在控制台录入数据 ababcea

    回车之后效果:

    调试界面效果:

  8. 此时到达findChar方法,快捷键F7,进入方法findChar

  9. 快捷键F8 接续执行,创建了map对象,变量区域显示

  10. 快捷键F8 接续执行,进入到循环中,循环变量i为 0,F8再继续执行,就获取到变量c赋值为字符‘a’ 字节值97

  11. 快捷键F8 接续执行,进入到判断语句中,因为该字符 不在Map集合键集中,再按F8执行,进入该判断中

  12. 快捷键F8 接续执行,循环结束,进入下次循环,此时map中已经添加一对儿元素

  13. 快捷键F8 接续执行,进入下次循环,再继续上面的操作,我们就可以看到代码每次是如何执行的了

  14. 如果不想继续debug,那么可以使用快捷键F9,程序正常执行到结束,程序结果在控制台显示

异常

概念

异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:

  • 异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

体系

异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

异常体系

Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。

    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

  • public String getMessage():获取发生异常的原因。

    提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。

简单的异常查看

分类

我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)

异常的分类

异常的产生过程解析

先运行下面的程序,程序会产生一个数组索引越界异常 ArrayIndexOfBoundsException 。我们通过图解来解析下异常产生的过程。

工具类

1
2
3
4
5
6
7
public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

测试类

1
2
3
4
5
6
7
8
public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}

上述程序执行过程图解:

异常产生过程解析

异常的处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

抛出异常throw

在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。

在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?

  1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。

  2. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象。

    throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

1
throw new 异常类名(参数);

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。

Objects非空判断

Objects由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),那么在它的源码中,对对象为null的值进行了抛出异常操作。

  • public static <T> T requireNonNull(T obj):查看指定引用对象不是null。

声明异常throws

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

声明异常格式:

1
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }	

捕获异常try…catch

如果异常出现的话,会立刻终止程序,所以我们得处理异常:

  1. 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
  2. 在方法中使用try-catch的语句块来处理异常。

try-catch的方式就是捕获异常。

  • 捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。
1
2
3
4
5
6
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}

try:该代码块中编写可能产生异常的代码。

catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。

注意:try和catch都不能单独使用,必须连用。

如何获取异常信息:

Throwable类中定义了一些查看方法:

  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。

包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

finally 代码块

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

什么时候的代码必须最终执行?

当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally的语法:

try…catch….finally:自身需要处理异常,最终还得关闭资源。

注意:finally不能单独使用。

比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

异常注意事项

  • 多个异常使用捕获又该如何处理呢?

    1. 多个异常分别处理。
    2. 多个异常一次捕获,多次处理。
    3. 多个异常一次捕获一次处理。

    一般我们是使用一次捕获多次处理方式,格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try{
    编写可能会出现异常的代码
    }catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
    处理异常的代码
    //记录日志/打印异常信息/继续抛出异常
    }catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
    处理异常的代码
    //记录日志/打印异常信息/继续抛出异常
    }

    注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。

  • 如果finally有return语句,永远返回finally中的结果,避免该情况.

  • 如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。

  • 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出

自定义异常

概述

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题等等。

在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: RegisterException。一个注册异常类。

异常类如何定义:

  1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception
  2. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException

多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

02_并发与并行

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

    进程

进程概念

线程

线程概念

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
      其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

创建线程类

继承Thread类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

线程

多线程原理

流程图:

线程流程图

程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

栈内存原理图当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

Thread类

构造方法:

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式

创建线程

实现Runnable接口

采用java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
    的线程对象。
  3. 调用线程对象的start()方法来启动线程。

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:
实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了**同步机制(synchronized)**来解决。

有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

  • 同步代码块synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
1
2
3
synchronized(同步锁){
需要同步操作的代码
}
  • 同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
    1. 锁对象 可以是任意类型。
    2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。

同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
1
2
3
public synchronized void method(){
可能会产生线程安全问题的代码
}

同步锁是谁?

对于非static方法,同步锁就是this。

对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。

线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
New(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待).

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

Timed Waiting 线程状态图:

计时等待

BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态,而这部分内容作为扩充知识点带领大家了解一下。

锁阻塞

Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

无限等待

线程状态图

我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

等待唤醒机制

线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

线程间通信

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(**wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify()**);在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

生产者和消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。

就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

1
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

线程池

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

线程池原理合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Lambda表达式

函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

面向对象的思想:

​ 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.

函数式编程思想:

​ 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

冗余的Runnable代码

传统写法

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo01Runnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
@Override
public void run() { // 覆盖重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start(); // 启动线程
}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

编程思想转换

做什么,而不是怎么做

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

生活举例

当我们需要从北京到上海时,可以选择高铁、汽车、骑行或是徒步。我们的真正目的是到达上海,而如何才能到达上海的形式并不重要,所以我们一直在探索有没有比高铁更好的方式——搭乘飞机。

而现在这种飞机(甚至是飞船)已经诞生:2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。

Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

1
(参数类型 参数名称) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。


java
https://zhstzzy.github.io/2022/06/28/Java/
作者
zhstzzy
发布于
2022年6月28日
许可协议