JavaSE1
Java语法规范
所有的Java语句必须以;
结尾!
无论是()
、[]
还是{}
,所有的括号必须一一匹配!
主方法的代码只能写在{}
中!
Java基础语法(面向过程)
在学习面向对象之前,我们需要了解面向过程的编程思维,如果你学习过C语言和Python就会很轻松!
变量和关键字
变量
变量就是一个可变的量,例如定义一个int类型的变量(int就是整数类型):
int a = 10;
a = 20;
a = 30;
我们能够随意更改它的值,也就是说它的值是随时可变的,我们称为变量。变量可以是类的变量,也可以是方法内部的局部变量(我们现阶段主要用局部变量,类变量在面向对象再讲解)
变量和C语言中的变量不同,Java中的变量是存放在JVM管理的内存中,C语言的变量存放在内存(某些情况下需要手动释放内存,而Java会自动帮助我们清理变量占据的内存)Java和C++很类似,但是没有指针!Java也叫C++--
Java是强类型语言,只有明确定义了变量之后,你才能使用!一旦被指定某个数据类型,那么它将始终被认为是对应的类型(和JS不一样!)
定义一个变量的格式如下:
[类型] [标识符(名字)] = [初始值(可选)]
int a = 10;
注意:标识符不能为以下内容:
- 标识符以由大小写字母、数字、下划线(_)和美元符号($)组成,但是不能以数字开头。
- 大小写敏感!
- 不能有空格、@、#、+、-、/ 等符号
- 应该使用有意义的名称,达到见名知意的目的,最好以小写字母开头
- 不可以是 true 和 false
- 不能与Java语言的关键字重名
关键字

包括基本数据类型、流程控制语句等,了解就行,不用去记,后面我们会一点一点带大家认识!
常量
常量就是无法修改值的变量,常量的值,只能定义一次:
final int a = 10;
a = 10; //报错!
常量前面必须添加final关键字(C语言里面是const,虽然Java也有,但是不能使用!)
这只是final关键字的第一个用法,后面还会有更多的用法。
注释
养成注释的好习惯,不然以后自己都看不懂自己的代码!注释包括单行注释和多行注释:
//我是单行注释
/**
* 我是
* 多行注释
*/
//TODO 待做标记
基本数据类型
Java中的数据类型分为基本数据类型和引用类型两大类,引用类型我们在面向对象时再提,基本数据类型是重点中的重点!首先我们需要了解有哪些类型。然后,我们需要知道的,并不是他们的精度如何,能够表示的范围有多大,而是为什么Java会给我们定义这些类型,计算机是怎么表示这些类型的,这样我们才能够更好的记忆他们的精度、表示的范围大小。所以,我们从计算机原理的角度出发,带领大家走进Java的基本数据类型。
这一部分稍微有点烧脑,但是是重中之重,如果你掌握了这些,任何相关的面试题都难不倒你!(如果你学习过计算机组成原理就很好理解了)
计算机中的二进制表示
在计算机中,所有的内容都是二进制形式表示。十进制是以10为进位,如9+1=10;二进制则是满2进位(因为我们的计算机是电子的,电平信号只有高位和低位,你也可以暂且理解为通电和不通电,高电平代表1,低电平代表0,由于只有0和1,因此只能使用2进制表示我们的数字!)比如1+1=10=2^1+0,一个位也叫一个bit,8个bit称为1字节,16个bit称为一个字,32个bit称为一个双字,64个bit称为一个四字,我们一般采用字节来描述数据大小。
十进制的7 -> 在二进制中为 111 = 2^2 + 2^1 + 2^0
现在有4个bit位,最大能够表示多大的数字呢?
- 最小:0000 => 0
- 最大:1111 => 23+22+21+20 => 8 + 4 + 2 + 1 = 15
在Java中,无论是小数还是整数,他们都要带有符号(和C语言不同,C语言有无符号数)所以,首位就作为我们的符号位,还是以4个bit为例,首位现在作为符号位(1代表负数,0代表正数):
- 最小:1111 => -(22+21+2^0) => -7
- 最大:0111 => +(22+21+2^0) => +7 => 7
现在,我们4bit能够表示的范围变为了-7~+7,这样的表示方式称为原码。
计算机中的加减法
原码
虽然原码表示简单,但是原码在做加减法的时候,很麻烦!以4bit位为例:
1+(-1) = 0001 + 1001 = 怎么让计算机去计算?(虽然我们知道该去怎么算,但是计算机不知道!)
我们得创造一种更好的表示方式!于是我们引入了反码:
反码
- 正数的反码是其本身
- 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反
经过上面的定义,我们再来进行加减法:
1+(-1) = 0001 + 1110 = 1111 => -0 (直接相加,这样就简单多了!)
思考:1111代表-0,0000代表+0,在我们实数的范围内,0有正负之分吗?
- 0既不是正数也不是负数,那么显然这样的表示依然不够合理!
补码
根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:
- 正数的补码就是其本身 (不变!)
- 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
其实现在就已经能够想通了,-0其实已经被消除了!我们再来看上面的运算:
1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了!)
所以现在,4bit位能够表示的范围是:-8~+7(Java使用的就是补码!)
以上内容是重点, 是一定要掌握的知识,这些知识是你在面试中的最终防线!有了这些理论基础,无论面试题如何变换,都能够通过理论知识来破解
整数类型
整数类型是最容易理解的类型!既然我们知道了计算机中的二进制数字是如何表示的,那么我们就可以很轻松的以二进制的形式来表达我们十进制的内容了。
在Java中,整数类型包括以下几个:
- byte 字节型 (8个bit,也就是1个字节)范围:-128~+127
- short 短整形(16个bit,也就是2个字节)范围:-32768~+32767
- int 整形(32个bit,也就是4个字节)最常用的类型!
- long 长整形(64个bit,也就是8个字节)最后需要添加l或L
long都装不下怎么办?BigInteger!
数字已经达到byte的最大值了,还能加吗?为了便于理解,以4bit为例:
0111 + 0001 = 1000 => -8(你没看错,就是这样!)
整数还能使用8进制、16进制表示:
- 十进制为15 = 八进制表示为017 = 十六进制表示为 0xF = 二进制表示 1111 (代码里面不能使用二进制!)
字符类型和字符串
在Java中,存在字符类型,它能够代表一个字符:
- char 字符型(16个bit,也就是2字节,它不带符号!)范围是0 ~ 65535
- 使用Unicode表示就是:\u0000 ~ \uffff
字符要用单引号扩起来!比如 char c = '淦';
字符其实本质也是数字,但是这些数字通过编码表进行映射,代表了不同的字符,比如字符'A'
的ASCII码就是数字65
,所以,char类型其实可以转换为上面的整数类型。
Java的char采用Unicode编码表(不是ASCII编码!),Unicode编码表包含ASCII的所有内容,同时还包括了全世界的语言,ASCII只有1字节,而Unicode编码是2字节,能够代表65536种文字,足以包含全世界的文字了!(我们编译出来的字节码文件也是使用Unicode编码的,所以利用这种特性,其实Java支持中文变量名称、方法名称甚至是类名)
既然char只能代表一个字符,那怎么才能包含一句话呢?(关于数组,我们这里先不了解,数组我们放在面向对象章节讲解)
String就是Java中的字符串类型(注意,它是一个类,创建出来的字符串本质是一个对象,不是我们的基本类型)字符串就像它的名字一样,代表一串字符,也就是一句完整的话。
字符串用双引号括起来!比如:String str = "一日三餐没烦恼";
小数类型
小数类型比较难理解(比较难理解指的是原理,不是使用)首先来看看Java中的小数类型包含哪些:
- float 单精度浮点型 (32bit,4字节)
- double 双精度浮点型(64bit,8字节)
思考:小数的范围该怎么定义呢?我们首先要了解的是小数在计算机里面是如何存放的:

根据国际标准 IEEE 754,任意一个二进制浮点数 V 可以表示成下面的形式:
V = (-1)^S × M × 2^E
(1)(-1)^S 表示符号位,当 S=0,V 为正数;当 S=1,V 为负数。
(2)M 表示有效数字,大于等于 1,小于 2,但整数部分的 1 不变,因此可以省略。(例如尾数为1111010,那么M实际上就是1.111010,尾数首位必须是1,1后面紧跟小数点,如果出现0001111这样的情况,去掉前面的0,移动1到首位;题外话:随着时间的发展,IEEE 754标准默认第一位为1,故为了能够存放更多数据,就舍去了第一位,比如保存1.0101 的时候, 只保存 0101,这样能够多存储一位数据)
(3)2^E 表示指数位。(用于移动小数点)
比如: 对于十进制的 5.25 对应的二进制为:101.01,相当于:1.0101*2^2。所以,S 为 0,M 为 1.0101,E 为 2。所以,对于浮点类型,最大值和最小值不仅取决于符号和尾数,还有它的阶码。我们在这里就不去计算了,想了解的可以去搜索相关资料。
思考:就算double有64bit位数,但是依然存在精度限制,如果我要进行高精度的计算,怎么办?BigDecimal!
布尔类型
布尔类型(boolean)只有true
和false
两种值,也就是要么为真,要么为假,布尔类型的变量通常用作流程控制判断语句。(C语言一般使用0表示false,除0以外的所有数都表示true)布尔类型占据的空间大小并未明确定义,而是根据不同的JVM会有不同的实现。
类型转换
隐式类型转换
隐式类型转换支持字节数小的类型自动转换为字节数大的类型,整数类型自动转换为小数类型,转换规则如下:
- byte→short(char)→int→long→float→double
问题:为什么long比float大,还能转换为float呢?小数的存储规则让float的最大值比long还大,只是可能会丢失某些位上的精度!
所以,如下的代码就能够正常运行:
byte b = 9;
short s = b;
int i = s;
long l = i;
float f = l;
double d = f;
System.out.println(d);
//输出 9.0
显示类型转换
显示类型转换也叫做强制类型转换,也就是说,违反隐式转换的规则,牺牲精度强行进行类型转换。
int i = 128;
byte b = (byte)i;
System.out.println(b);
//输出 -128
为什么结果是-128?精度丢失了!
- int 类型的128表示:00000000 00000000 00000000 10000000
- byte类型转换后表示:xxxxxxxx xxxxxxxx xxxxxxxx 10000000 => -128
数据类型自动提升
在参与运算时(也可以位于表达式中时,自增自减除外),所有的byte型、short型和char的值将被提升到int型:
byte b = 105;
b = b + 1; //报错!
System.out.println(b);
这个特性是由 Java虚拟机规范 定义的,也是为了提高运行的效率。其他的特性还有:
- 如果一个操作数是long型,计算结果就是long型
- 如果一个操作数是float型,计算结果就是float型
- 如果一个操作数是double型,计算结果就是double型
运算符
赋值和算术运算符
赋值运算符=
是最常用的运算符,其实就是将我们等号右边的结果,传递给等号左边的变量,例如:
int a = 10;
int b = 1 + 8;
int c = 5 * 5;
算术运算符也就是我们在小学阶段学习的+
-
*
/
%
,分别代表加减乘除还有取余,例如:
int a = 2;
int b = 3;
int c = a * b;
//结果为6
需要注意的是,+
还可以用作字符串连接符使用:
System.out.println("lbw" + "nb"); //lbwnb
当然,字符串可以直接连接其他类型,但是会全部当做字符串处理:
int a = 7, b = 15;
System.out.println("lbw" + a + b); //lbw715
算术运算符还包括++
和--
也就是自增和自减,以自增为例:
int a = 10;
a++;
System.out.println(a); //输出为11
自增自减运算符放在变量的前后的返回值是有区别的:
int a = 10;
System.out.println(a++); //10 (先返回值,再自增)
System.out.println(a); //11
int a = 10;
System.out.println(++a); //11 (先自增,再返回值)
System.out.println(a); //11
int a = 10;
int b = 2;
System.out.println(b+++a++); //猜猜看结果是多少
为了使得代码更简洁,你还可以使用扩展的赋值运算符,包括+=
、-=
、/=
、*=
、%=
,和自增自减类似,先执行运算,再返回结果,同时自身改变:
int a = 10;
System.out.println(a += 2); //等价于 a = a + 2
关系运算符
关系运算符的结果只能是布尔类型,也就是要么为真要么为假,关系运算符包括:
> < == //大于小于等于
>= <= != //大于等于,小于等于,不等于
关系运算符一般只用于基本类型的比较,运算结果只能是boolean:
int a = 10;
int b = 2;
boolean x = a > b;
System.out.println(x);
//结果为 true
逻辑运算符
逻辑运算符两边只能是boolean类型或是关系/逻辑运算表达式,返回值只能是boolean类型!逻辑运算符包括:
&& //与运算,要求两边同时为true才能返回true
|| //或运算,要求两边至少要有一个为true才能返回true
! //非运算,一般放在表达式最前面,表达式用括号扩起来,表示对表达式的结果进行反转
实际案例来看看:
int a = 10;
int b = 2;
boolean x = a > b && a < b; //怎么可能同时满足呢
System.out.println(x); //false
int a = 10;
int b = 2;
boolean x = a > b || a <= b; //一定有一个满足!
System.out.println(x); //true
int a = 10;
int b = 2;
boolean x = !(a > b); //对结果进行反转,本来应该是true
System.out.println(x); //false
位运算符
& //按位与,注意,返回的是运算后的同类型值,不是boolean!
| //按位或
^ //按位异或 0 ^ 0 = 0
~ //按位非
按位运算实际上是根据值的二进制编码来计算结果,例如按位与,以4bit为例:
0101 & 0100 = 0100 (只有同时为1对应位才得1)
int a = 7, b = 15;
System.out.println(a & b); //结果为7
三目运算符
三目运算符其实是为了简化代码而生,可以根据条件是否满足来决定返回值,格式如下:
int a = 7, b = 15;
String str = a > b ? "行" : "不行"; // 判断条件(只能是boolean,或返回boolean的表达式) ? 满足的返回值 : 不满足的返回值
System.out.println("汉堡做的行不行?"+str); //汉堡做的行不行?不行
理解三目运算符,就很容易理解后面的if-else语句了。
流程控制
我们的程序都是从上往下依次运行的,但是,仅仅是这样还不够,我们需要更加高级的控制语句来帮我进行更灵活的控制。比如,判断用户输入的数字,大于1则输出ok,小于1则输出no,这时我们就需要用到选择结构来帮助我们完成条件的判断和程序的分支走向。学习过C语言就很轻松!
选择结构
选择结构包含if和switch类型,选择结构能够帮助我们根据条件判断,再执行哪一块代码。
if语句
就像上面所说,判断用户输入的数字,大于1则输出ok,小于1则输出no,要实现这种效果,我们首先可以采用if语句:
if(判断条件){
//判断成功执行的内容
}else{
//判断失败执行的内容
}
//if的内容执行完成后,后面的内容正常执行
其中,else
语句不是必须的。
现在,又来了一个新的需求,用户输入的是1打印ok,输入2,打印yes,其他打印no,那么这样就需要我们进行多种条件的判断了,当然if能进行多分支判断:
if(判断条件1){
//判断成功执行的内容
}else if(判断条件2){
//再次判断,如果判断成功执行的内容
}else{
//上面的都没成功,只能走这里
}
同样,else
语句不是必须的。
现在,又来了一个新的需求,用户输入1之后,在判断用户下一次输入的是什么,如果是1,打印yes,不是就打印no,这样就可以用嵌套if了:
if(判断条件1){
//前提是判断条件1要成功才能进来!
if(判断条件2){
//判断成功执行的内容
}else{
//判断失败执行的内容
}
}
switch语句
我们不难发现,虽然else-if
能解决多分支判断的问题,但是效率实在是太低了,多分支if采用的是逐级向下判断,显然费时费力,那么有没有一直更专业的解决多分支判断问题的东西呢?
switch(判断主体){
case 值1:
//运行xxx
break; //break用于跳出switch语句,不添加会导致程序继续向下运行!
case 值2:
//运行xxx
break;
case 值3:
//运行xxx
break;
}
在上述语句中,只有判断主体等于case后面的值时,才会执行case中的语句,同时需要使用break来跳出switch语句,否则会继续向下运行!
为什么switch效率更高呢,因为switch采用二分思想进行查找(这也是为什么switch只能判断值相等的原因),能够更快地找到我们想要的结果!
循环结构
小明想向小红表白,于是他在屏幕上打印了520个 "我爱你",我们用Java该如何实现呢?
for语句
for语句是比较灵活的循环控制语句,一个for语句的定义如下:
for(初始条件;循环条件;更新){
//循环执行的内容
}
//循环结束后,继续执行
- 初始条件:循环开始时的条件,一般用于定义控制循环的变量。
- 循环条件:每轮循环开始之前,进行一次判断,如果满足则继续,不满足则结束,要求为boolean变量或是boolean表达式。
- 更新:每轮循环结束后都会执行的内容,一般写增量表达式。
初始条件、循环条件、更新条件不是缺一不可,甚至可以都缺!
for(int i = 0;i < 520;i++){
System.out.println("我爱你");
}
for(;;){
//这里的内容将会永远地进行下去!
}
增强for循环在数组时再讲解!
while循环
while循环和for循环类似,但是它更加的简单,只需要添加维持循环的判断条件即可!
while(循环条件){
//循环执行的内容
}
和for一样,每次循环开始,当循环条件不满足时,自动退出!那么有时候我们希望先执行了我们的代码再去判断怎么办呢,我们可以使用do-while语句:
do{
//执行内容
}while(循环条件);
一定会先执行do里面的内容,再做判断!
思考:
for(;;){
}
while(true){
}
//它们的性能谁更高?
面向过程编程实战(基础+算法)
打印九九乘法表
简单:将九九乘法表打印到控制台。
求1000以内的水仙花数
中等:打印1000以内所有满足水仙花的数,“水仙花数”是指一个三位数其各位数字的立方和等于该数本身,例如153是“水仙花数”,因为:153 = 1^3 + 5^3 + 3^3
青蛙跳台阶问题
困难:一共有n个台阶,一只青蛙每次只能跳一阶或是两阶,那么一共有多少种跳到顶端的方案?例如n=2,那么一共有两种方案,一次性跳两阶或是每次跳一阶。
动态规划:其实,就是利用,上次得到的结果,给下一次作参考,下一次就能利用上次的结果快速得到结果,依次类推
不对啊,别的教程都讲了数组、方法,怎么我们还没讲就进入面向对象了呢?
- 数组在Java中,并非基本类型,数组是编程不可见的对象类型,学习了面向对象再来理解,会更加容易!
- 方法在Java中是类具有的属性,所以,在了解了对象类型之后,再来了解方法,就更加简单了!
Java对象和多态 (面向对象)
面向对象基础
面向对象程序设计(Object Oriented Programming)
对象基于类创建,类相当于一个模板,对象就是根据模板创建出来的实体(就像做月饼,我们要做一个月饼首先需要一个模具,模具就是我们的类,而做出来的月饼,就是类的实现,也叫做对象),类是抽象的数据类型,并不能代表某一个具体的事物,类是对象的一个模板。类具有自己的属性,包括成员变量、成员方法等,我们可以调用类的成员方法来让类进行一些操作。
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
System.out.println("你输入了:"+str);
sc.close();
所有的对象,都需要通过new
关键字创建,基本数据类型不是对象!Java不是纯面对对象语言!
不是基本类型的变量,都是引用类型,引用类型变量代表一个对象,而基本数据类型变量,保存的是基本数据类型的值,我们可以通过引用来对对象进行操作。(最好不要理解为引用指向对象的地址,初学者不要谈内存,学到JVM时再来讨论)
对象占用的内存由JVM统一管理,不需要手动释放内存,当一个对象不再使用时(比如失去引用或是离开了作用域)会被JVM自动清理,内存管理更方便!
类的基本结构
为了快速掌握,我们自己创建一个自己的类,创建的类文件名称应该和类名一致。
成员变量
在类中,可以包含许多的成员变量,也叫成员属性,成员字段(field)通过.
来访问我们类中的成员变量,我们可以通过类创建的对象来访问和修改这些变量。成员变量是属于对象的!
public class Test {
int age;
String name;
}
public static void main(String[] args) {
Test test = new Test();
test.name = "奥利给";
System.out.println(test.name);
}
成员变量默认带有初始值,也可以自己定义初始值。
成员方法
我们之前的学习中接触过方法(Method)吗?主方法!
public static void main(String[] args) {
//Body
}
方法是语句的集合,是为了完成某件事情而存在的。完成某件事情,可以有结果,也可以做了就做了,不返回结果。比如计算两个数字的和,我们需要得到计算后的结果,所以说方法需要有返回值;又比如,我们只想吧数字打印在控制台,只需要打印就行,不用给我结果,所以说方法不需要有返回值。
方法的定义和使用
在类中,我们可以定义自己的方法,格式如下:
[返回值类型] 方法名称([参数]){
//方法体
return 结果;
}
- 返回值类型:可以是引用类型和基本类型,还可以是void,表示没有返回值
- 方法名称:和标识符的规则一致,和变量一样,规范小写字母开头!
- 参数:例如方法需要计算两个数的和,那么我们就要把两个数到底是什么告诉方法,那么它们就可以作为参数传入方法
- 方法体:方法具体要干的事情
- 结果:方法执行的结果通过return返回(如果返回类型为void,可以省略return)
非void方法中,return
关键字不一定需要放在最后,但是一定要保证方法在任何情况下都具有返回值!
int test(int a){
if(a > 0){
//缺少retrun语句!
}else{
return 0;
}
}
return
也能用来提前结束整个方法,无论此时程序执行到何处,无论return位于哪里,都会立即结束个方法!
void main(String[] args) {
for (int i = 0; i < 10; i++) {
if(i == 1) return; //在循环内返回了!和break区别?
}
System.out.println("淦"); //还会到这里吗?
}
传入方法的参数,如果是基本类型,会在调用方法的时候,对参数的值进行复制,方法中的参数变量,不是我们传入的变量本身!
public static void main(String[] args) {
int a = 10, b = 20;
new Test().swap(a, b);
System.out.println("a="+a+", b="+b);
}
public class Test{
void swap(int a, int b){ //传递的仅仅是值而已!
int temp = a;
a = b;
b = temp;
}
}
传入方法的参数,如果是引用类型,那么传入的依然是该对象的引用!(类似于C语言的指针)
public class B{
String name;
}
public class A{
void test(B b){ //传递的是对象的引用,而不是值
System.out.println(b.name);
}
}
public static void main(String[] args) {
int a = 10, b = 20;
B b = new B();
b.name = "lbw";
new A().test(b);
System.out.println("a="+a+", b="+b);
}
方法之间可以相互调用
void a(){
//xxxx
}
void b(){
a();
}
当方法在自己内部调用自己时,称为递归调用(递归很危险,慎重!)
int a(){
return a();
}
成员方法和成员变量一样,是属于对象的,只能通过对象去调用!
对象设计练习
- 学生应该具有以下属性:名字、年龄
- 学生应该具有以下行为:学习、运动、说话
方法的重载
一个类中可以包含多个同名的方法,但是需要的形式参数不一样。(补充:形式参数就是定义方法需要的参数,实际参数就传入的参数)方法的返回类型,可以相同,也可以不同,但是仅返回类型不同,是不允许的!
public class Test {
int a(){ //原本的方法
return 1;
}
int a(int i){ //ok,形参不同
return i;
}
void a(byte i){ //ok,返回类型和形参都不同
}
void a(){ //错误,仅返回值类型名称不同不能重载
}
}
现在我们就可以使用不同的参数,但是支持调用同样的方法,执行一样的逻辑:
public class Test {
int sum(int a, int b){ //只有int支持,不灵活!
return a+b;
}
double sum(double a, double b){ //重写一个double类型的,就支持小数计算了
return a+b;
}
}
现在我们有很多种重写的方法,那么传入实参后,到底进了哪个方法呢?
public class Test {
void a(int i){
System.out.println("调用了int");
}
void a(short i){
System.out.println("调用了short");
}
void a(long i){
System.out.println("调用了long");
}
void a(char i){
System.out.println("调用了char");
}
void a(double i){
System.out.println("调用了double");
}
void a(float i){
System.out.println("调用了float");
}
public static void main(String[] args) {
Test test = new Test();
test.a(1); //直接输入整数
test.a(1.0); //直接输入小数
short s = 2;
test.a(s); //会对号入座吗?
test.a(1.0F);
}
}
构造方法
构造方法(构造器)没有返回值,也可以理解为,返回的是当前对象的引用!每一个类都默认自带一个无参构造方法。
//反编译结果
package com.test;
public class Test {
public Test() { //即使你什么都不编写,也自带一个无参构造方法,只是默认是隐藏的
}
}
反编译其实就是把我们编译好的class文件变回Java源代码。
Test test = new Test(); //实际上存在Test()这个的方法,new关键字就是用来创建并得到引用的
// new + 你想要使用的构造方法
这种方法没有写明返回值,但是每个类都必须具有这个方法!只有调用类的构造方法,才能创建类的对象!
类要在一开始准备的所有东西,都会在构造方法里面执行,完成构造方法的内容后,才能创建出对象!
一般最常用的就是给成员属性赋初始值:
public class Student {
String name;
Student(){
name = "伞兵一号";
}
}
我们可以手动指定有参构造,当遇到名称冲突时,需要用到this关键字
public class Student {
String name;
Student(String name){ //形参和类成员变量冲突了,Java会优先使用形式参数定义的变量!
this.name = name; //通过this指代当前的对象属性,this就代表当前对象
}
}
//idea 右键快速生成!
注意,this只能用于指代当前对象的内容,因此,只有属于对象拥有的部分才可以使用this,也就是说,只能在类的成员方法中使用this,不能在静态方法中使用this关键字。
在我们定义了新的有参构造之后,默认的无参构造会被覆盖!
//反编译后依然只有我们定义的有参构造!
如果同时需要有参和无参构造,那么就需要用到方法的重载!手动再去定义一个无参构造。
public class Student {
String name;
Student(){
}
Student(String name){
this.name = name;
}
}
成员变量的初始化始终在构造方法执行之前
public class Student {
String a = "sadasa";
Student(){
System.out.println(a);
}
public static void main(String[] args) {
Student s = new Student();
}
}
静态变量和静态方法
静态变量和静态方法是类具有的属性(后面还会提到静态类、静态代码块),也可以理解为是所有对象共享的内容。我们通过使用static
关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。那么,一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。
public class Student {
static int a;
}
public static void main(String[] args) {
Student s1 = new Student();
s1.a = 10;
Student s2 = new Student();
System.out.println(s2.a);
}
不推荐使用对象来调用,被标记为静态的内容,可以直接通过类名.xxx
的形式访问
public static void main(String[] args) {
Student.a = 10;
System.out.println(Student.a);
}
简述类加载机制
类并不是在一开始就全部加载好,而是在需要时才会去加载(提升速度)以下情况会加载类:
- 访问类的静态变量,或者为静态变量赋值
- new 创建类的实例(隐式加载)
- 调用类的静态方法
- 子类初始化时
- 其他的情况会在讲到反射时介绍
所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。
public class Student {
static int a = test(); //直接调用静态方法,只能调用静态方法
Student(){
System.out.println("构造类对象");
}
static int test(){ //静态方法刚加载时就有了
System.out.println("初始化变量a");
return 1;
}
}
思考:下面这种情况下,程序能正常运行吗?如果能,会输出什么内容?
public class Student {
static int a = test();
static int test(){
return a;
}
public static void main(String[] args) {
System.out.println(Student.a);
}
}
定义和赋值是两个阶段,在定义时会使用默认值(上面讲的,类的成员变量会有默认值)定义出来之后,如果发现有赋值语句,再进行赋值,而这时,调用了静态方法,所以说会先去加载静态方法,静态方法调用时拿到a,而a这时仅仅是刚定义,所以说还是初始值,最后得到0
代码块和静态代码块
代码块在对象创建时执行,也是属于类的内容,但是它在构造方法执行之前执行(和成员变量初始值一样),且每创建一个对象时,只执行一次!(相当于构造之前的准备工作)
public class Student {
{
System.out.println("我是代码块");
}
Student(){
System.out.println("我是构造方法");
}
}
静态代码块和上面的静态方法和静态变量一样,在类刚加载时就会调用;
public class Student {
static int a;
static {
a = 10;
}
public static void main(String[] args) {
System.out.println(Student.a);
}
}
String和StringBuilder类
字符串类是一个比较特殊的类,他是Java中唯一重载运算符的类!(Java不支持运算符重载,String是特例)
String的对象直接支持使用+
或+=
运算符来进行拼接,并形成新的String对象!(String的字符串是不可变的!)
String a = "dasdsa", b = "dasdasdsa";
String l = a+b;
System.out.println(l);
大量进行字符串的拼接似乎不太好,编译器是很聪明的,String的拼接有可能会被编译器优化为StringBuilder来减少对象创建(对象频繁创建时很费时间同时占内存的!)
String result="String"+"and"; //会被优化成一句!
String str1="String";
String str2="and";
String result=str1+str2;
//变量随时可变,在编译时无法确定result的值,那么只能在运行时再去确定
String str1="String";
String str2="and";
String result=(new StringBuilder(String.valueOf(str1))).append(str2).toString();
//使用StringBuilder,会采用类似于第一种实现,显然会更快!
StringBuilder也是一个类,但是它能够存储可变长度的字符串!
StringBuilder builder = new StringBuilder();
builder
.append("a")
.append("bc")
.append("d"); //链式调用
String str = builder.toString();
System.out.println(str);
包和访问控制
包声明和导入
包其实就是用来区分类位置的东西,也可以用来将我们的类进行分类,类似于C++中的namespace!
package com.test;
public class Test{
}
包其实是文件夹,比如com.test就是一个com文件夹中包含一个test文件夹,再包含我们Test类。
一般包按照个人或是公司域名的规则倒过来写 顶级域名.一级域名.二级域名
com.java.xxxx
如果需要使用其他包里面的类,那么我们需要import
(类似于C/C++中的include)
import com.test.Student;
也可以导入包下的全部(一般导入会由编译器自带帮我们补全,但是一定要记得我们需要导包!)
import com.test.*
Java默认为我们导入了以下的包,不需要去声明
import java.lang.*
静态导入
静态导入可以直接导入某个类的静态方法或者是静态变量,导入后,相当于这个方法或是类在定义在当前类中,可以直接调用该方法。
import static com.test.ui.Student.test;
public class Main {
public static void main(String[] args) {
test();
}
}
静态导入不会进行类的初始化!
访问控制
Java支持对类属性访问的保护,也就是说,不希望外部类访问类中的属性或是方法,只允许内部调用,这种情况下我们就需要用到权限控制符。

权限控制符可以声明在方法、成员变量、类前面,一旦声明private,只能类内部访问!
public class Student {
private int a = 10; //具有私有访问权限,只能类内部访问
}
public static void main(String[] args) {
Student s = new Student();
System.out.println(s.a); //还可以访问吗?
}
和文件名称相同的类,只能是public,并且一个java文件中只能有一个public class!
// Student.java
public class Student {
}
class Test{ //不能添加权限修饰符!只能是default
}
数组类型
假设出现一种情况,我想记录100个数字,定义100个变量还可行吗?
我们可以使用到数组,数组是相同类型数据的有序集合。数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)其中存放的每一个数据称为数组的一个元素,数组的下标是从0开始,也就是第一个元素的索引是0!
int[] arr = new int[10]; //需要new关键字来创建!
String[] arr2 = new String[10];
数组本身也是类(编程不可见,C++写的),不是基本数据类型!
int[] arr = new int[10];
System.out.println(arr.length); //数组有成员变量!
System.out.println(arr.toString()); //数组有成员方法!
一维数组
一维数组中,元素是依次排列的(线性),每个数组元素可以通过下标来访问!声明格式如下:
类型[] 变量名称 = new 类型[数组大小];
类型 变量名称n = new 类型[数组大小]; //支持C语言样式,但不推荐!
类型[] 变量名称 = new 类型[]{...}; //静态初始化(直接指定值和大小)
类型[] 变量名称 = {...}; //同上,但是只能在定义时赋值
创建出来的数组每个元素都有默认值(规则和类的成员变量一样,C语言创建的数组需要手动设置默认值),我们可以通过下标去访问:
int[] arr = new int[10];
arr[0] = 626;
System.out.println(arr[0]);
System.out.println(arr[1]);
我们可以通过数组变量名称.length
来获取当前数组长度:
int[] arr = new int[]{1, 2, 3};
System.out.println(arr.length); //打印length成员变量的值
数组在创建时,就固定长度,不可更改!访问超出数组长度的内容,会出现错误!
String[] arr = new String[10];
System.out.println(arr[10]); //出现异常!
//Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 11
// at com.test.Application.main(Application.java:7)
思考:能不能直接修改length的值来实现动态扩容呢?
int[] arr = new int[]{1, 2, 3};
arr.length = 10;
数组做实参,因为数组也是类,所以形参得到的是数组的引用而不是复制的数组,操作的依然是数组对象本身
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3};
test(arr);
System.out.println(arr[0]);
}
private static void test(int[] arr){
arr[0] = 2934;
}
数组的遍历
如果我们想要快速打印数组中的每一个元素,又怎么办呢?
传统for循环
我们很容易就联想到for循环
int[] arr = new int[]{1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
foreach
传统for循环虽然可控性高,但是不够省事,要写一大堆东西,有没有一种省事的写法呢?
int[] arr = new int[]{1, 2, 3};
for (int i : arr) {
System.out.println(i);
}
foreach属于增强型的for循环,它使得代码更简洁,同时我们能直接拿到数组中的每一个数字。
二维数组
二维数组其实就是存放数组的数组,每一个元素都存放一个数组的引用,也就相当于变成了一个平面。
//三行两列
int[][] arr = { {1, 2},
{3, 4},
{5, 6}};
System.out.println(arr[2][1]);
二维数组的遍历同一维数组一样,只不过需要嵌套循环!
int[][] arr = new int[][]{ {1, 2},
{3, 4},
{5, 6}};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
System.out.println(arr[i][j]);
}
}
多维数组
不止二维数组,还存在三维数组,也就是存放数组的数组的数组,原理同二维数组一样,逐级访问即可。
可变长参数
可变长参数其实就是数组的一种应用,我们可以指定方法的形参为一个可变长参数,要求实参可以根据情况动态填入0个或多个,而不是固定的数量
public static void main(String[] args) {
test("AAA", "BBB", "CCC"); //可变长,最后都会被自动封装成一个数组
}
private static void test(String... test){
System.out.println(test[0]); //其实参数就是一个数组
}
由于是数组,所以说只能使用一种类型的可变长参数,并且可变长参数只能放在最后一位!
实战:三大基本排序算法
现在我们有一个数组,但是数组里面的数据是乱序排列的,如何使它变得有序?
int[] arr = {8, 5, 0, 1, 4, 9, 2, 3, 6, 7};
排序是编程的一个重要技能,掌握排序算法,你的技术才能更上一层楼,很多的项目都需要用到排序!三大排序算法:
- 冒泡排序
冒泡排序就是冒泡,其实就是不断使得我们无序数组中的最大数向前移动,经历n轮循环逐渐将每一个数推向最前。
- 插入排序
插入排序其实就跟我们打牌是一样的,我们在摸牌的时候,牌堆是乱序的,但是我们一张一张摸到手中进行排序,使得它变成了有序的!
- 选择排序
选择排序其实就是每次都选择当前数组中最大的数排到最前面!
封装、继承和多态
封装、继承和多态是面向对象编程的三大特性。
封装
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要同getter和setter方法来查看和设置变量。
设想:学生小明已经创建成功,正常情况下能随便改他的名字和年龄吗?
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
也就是说,外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字。
public void setName(String name) {
if(name.contains("小")) return;
this.name = name;
}
单独给外部开放设置名称的方法,因为我还需要做一些额外的处理,所以说不能给外部直接操作成员变量的权限!
封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现。
封装就是通过访问权限控制来实现的。
继承
继承属于非常重要的内容,在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。
现在学生分为两种,艺术生和体育生,他们都是学生的分支,但是他们都有自己的方法:
public class SportsStudent extends Student{ //通过extends关键字来继承父类
public SportsStudent(String name, int age) {
super(name, age); //必须先通过super关键字(指代父类),实现父类的构造方法!
}
public void exercise(){
System.out.println("我超勇的!");
}
}
public class ArtStudent extends Student{
public ArtStudent(String name, int age) {
super(name, age);
}
public void art(){
System.out.println("随手画个毕加索!");
}
}
子类具有父类的全部属性,protected可见但外部无法使用(包括private
属性,不可见,无法使用),同时子类还能有自己的方法。继承只能继承一个父类,不支持多继承!
每一个子类必须定义一个实现父类构造方法的构造方法,也就是需要在构造方法开始使用super()
,如果父类使用的是默认构造方法,那么子类不用手动指明。
所有类都默认继承自Object类,除非手动指定类型,但是依然改变不了最顶层的父类是Object类。所有类都包含Object类中的方法,比如:
public static void main(String[] args) {
Object obj = new Object;
System.out.println(obj.hashCode()); //求对象的hashcode,默认是对象的内存地址
System.out.println(obj.equals(obj)); //比较对象是否相同,默认比较的是对象的内存地址,也就是等同于 ==
System.out.println(obj.toString()); //将对象转换为字符串,默认生成对象的类名称+hashcode
}
关于Object类的其他方法,我们会在Java多线程中再来提及。
多态
多态是同一个行为具有多个不同表现形式或形态的能力。也就是同样的方法,由于实现类不同,执行的结果也不同!
方法的重写
我们之前学习了方法的重载,方法的重写和重载是不一样的,重载是原有的方法逻辑不变的情况下,支持更多参数的实现,而重写是直接覆盖原有方法!
//父类中的study
public void study(){
System.out.println("学习");
}
//子类中的study
@Override //声明这个方法是重写的,但是可以不要,我们现阶段不接触
public void study(){
System.out.println("给你看点好康的");
}
再次定义同样的方法后,父类的方法就被覆盖!子类还可以给父类方法提升访问权限!
public static void main(String[] args) {
SportsStudent student = new SportsStudent("lbw", 20);
student.study(); //输出子类定义的内容
}
思考:静态方法能被重写吗?
当我们在重写方法时,不仅想使用我们自己的逻辑,同时还希望执行父类的逻辑(也就是调用父类的方法)怎么办呢?
public void study(){
super.study();
System.out.println("给你看点好康的");
}
同理,如果想访问父类的成员变量,也可以使用super关键字来访问,注意,子类可以具有和父类相同的成员变量!而在方法中访问的默认是 形参列表中 > 当前类的成员变量 > 父类成员变量
public void setTest(int test){
test = 1;
this.test = 1;
super.test = 1;
}
再谈类型转换
我们曾经学习过基本数据类型的类型转换,支持一种数据类型转换为另一种数据类型,而我们的类也是支持类型转换的(仅限于存在亲缘关系的类之间进行转换)比如子类可以直接向上转型:
Student student = new SportsStudent("lbw", 20); //父类变量引用子类实例
student.study(); //得到依然是具体实现的结果,而不是当前类型的结果
我们也可以把已经明确是由哪个类实现的父类引用,强制转换为对应的类型:
Student student = new SportsStudent("lbw", 20); //是由SportsStudent进行实现的
//... do something...
SportsStudent ps = (SportsStudent)student; //让它变成一个具体的子类
ps.sport(); //调用具体实现类的方法
这样的类型转换称为向下转型。
instanceof关键字
那么我们如果只是得到一个父类引用,但是不知道它到底是哪一个子类的实现怎么办?我们可以使用instanceof关键字来实现,它能够进行类型判断!
private static void test(Student student){
if (student instanceof SportsStudent){
SportsStudent sportsStudent = (SportsStudent) student;
sportsStudent.sport();
}else if (student instanceof ArtStudent){
ArtStudent artStudent = (ArtStudent) student;
artStudent.art();
}
}
通过进行类型判断,我们就可以明确类的具体实现到底是哪个类!
思考:student instanceof Student
的结果是什么?
再谈final关键字
我们目前只知道final
关键字能够使得一个变量的值不可更改,那么如果在类前面声明final,会发生什么?
public final class Student { //类被声明为终态,那么它还能被继承吗
}
类一旦被声明为终态,将无法再被继承,不允许子类的存在!而方法被声明为final呢?
public final void study(){ //还能重写吗
System.out.println("学习");
}
如果类的成员属性被声明为final,那么必须在构造方法中或是在定义时赋初始值!
private final String name; //引用类型不允许再指向其他对象
private final int age; //基本类型值不允许发生改变
public Student(String name, int age) {
this.name = name;
this.age = age;
}
学习完封装继承和多态之后,我们推荐在不会再发生改变的成员属性上添加final关键字,JVM会对添加了final关键字的属性进行优化!
抽象类
类本身就是一种抽象,而抽象类,把类还要抽象,也就是说,抽象类可以只保留特征,而不保留具体呈现形态,比如方法可以定义好,但是我可以不去实现它,而是交由子类来进行实现!
public abstract class Student { //抽象类
public abstract void test(); //抽象方法
}
通过使用abstract
关键字来表明一个类是一个抽象类,抽象类可以使用abstract
关键字来表明一个方法为抽象方法,也可以定义普通方法,抽象方法不需要编写具体实现(无方法体)但是必须由子类实现(除非子类也是一个抽象类)!
抽象类由于不是具体的类定义,因此无法直接通过new关键字来创建对象!
Student s = new Student(){ //只能直接创建带实现的匿名内部类!
public void test(){
}
}
因此,抽象类一般只用作继承使用!抽象类使得继承关系之间更加明确:
public void study(){ //现在只能由子类编写,父类没有定义,更加明确了多态的定义!同一个方法多种实现!
System.out.println("给你看点好康的");
}
接口
接口甚至比抽象类还抽象,他只代表某个确切的功能!也就是只包含方法的定义,甚至都不是一个类!接口包含了一些列方法的具体定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)
public interface Eat {
void eat();
}
通过使用interface
关键字来表明是一个接口(注意,这里class关键字被替换为了interface)接口只能包含public
权限的抽象方法!(Java8以后可以有默认实现)我们可以通过声明default
关键字来给抽象方法一个默认实现:
public interface Eat {
default void eat(){
//do something...
}
}
接口中定义的变量,默认为public static final
public interface Eat {
int a = 1;
void eat();
}
一个类可以实现很多个接口,但是不能理解为多继承!(实际上实现接口是附加功能,和继承的概念有一定出入,顶多说是多继承的一种替代方案)一个类可以附加很多个功能!
public class SportsStudent extends Student implements Eat, ...{
@Override
public void eat() {
}
}
类通过implements
关键字来声明实现的接口!每个接口之间用逗号隔开!
实现接口的类也能通过instanceof关键字判断,也支持向上和向下转型!
内部类
类中可以存在一个类!各种各样的长相怪异的代码就是从这里开始出现的!
成员内部类
我们的类中可以在嵌套一个类:
public class Test {
class Inner{ //类中定义的一个内部类
}
}
成员内部类和成员变量和成员方法一样,都是属于对象的,也就是说,必须存在外部对象,才能创建内部类的对象!
public static void main(String[] args) {
Test test = new Test();
Test.Inner inner = test.new Inner(); //写法有那么一丝怪异,但是没毛病!
}
静态内部类
静态内部类其实就和类中的静态变量和静态方法一样,是属于类拥有的,我们可以直接通过类名.
去访问:
public class Test {
static class Inner{
}
}
public static void main(String[] args) {
Test.Inner inner = new Test.Inner(); //不用再创建外部类对象了!
}
局部内部类
对,你没猜错,就是和局部变量一样哒~
public class Test {
public void test(){
class Inner{
}
Inner inner = new Inner();
}
}
反正我是没用过!内部类 -> 累不累 -> 反正我累了!
匿名内部类
匿名内部类才是我们的重点,也是实现lambda表达式的原理!匿名内部类其实就是在new的时候,直接对接口或是抽象类的实现:
public static void main(String[] args) {
Eat eat = new Eat() {
@Override
public void eat() {
//DO something...
}
};
}
我们不用单独去创建一个类来实现,而是可以直接在new的时候写对应的实现!但是,这样写,无法实现复用,只能在这里使用!
lambda表达式
读作λ
表达式,它其实就是我们接口匿名实现的简化,比如说:
public static void main(String[] args) {
Eat eat = new Eat() {
@Override
public void eat() {
//DO something...
}
};
}
public static void main(String[] args) {
Eat eat = () -> {}; //等价于上述内容
}
lambda表达式(匿名内部类)只能访问外部的final类型或是隐式final类型的局部变量!
为了方便,JDK默认就为我们提供了专门写函数式的接口,这里只介绍Consumer
枚举类
假设现在我们想给小明添加一个状态(跑步、学习、睡觉),外部可以实时获取小明的状态:
public class Student {
private final String name;
private final int age;
private String status;
//...
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}
但是这样会出现一个问题,如果我们仅仅是存储字符串,似乎外部可以不按照我们规则,传入一些其他的字符串。这显然是不够严谨的!
有没有一种办法,能够更好地去实现这样的状态标记呢?我们希望开发者拿到使用的就是我们定义好的状态,我们可以使用枚举类!
public enum Status {
RUNNING, STUDY, SLEEP //直接写每个状态的名字即可,分号可以不打,但是推荐打上
}
使用枚举类也非常方便,我们只需要直接访问即可
public class Student {
private final String name;
private final int age;
private Status status;
//...
public void setStatus(Status status) { //不再是String,而是我们指定的枚举类型
this.status = status;
}
public Status getStatus() {
return status;
}
}
public static void main(String[] args) {
Student student = new Student("小明", 18);
student.setStatus(Status.RUNNING);
System.out.println(student.getStatus());
}
枚举类型使用起来就非常方便了,其实枚举类型的本质就是一个普通的类,但是它继承自Enum
类,我们定义的每一个状态其实就是一个public static final
的Status类型成员变量!
// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
public static final com.test.Status RUNNING;
public static final com.test.Status STUDY;
public static final com.test.Status SLEEP;
public static com.test.Status[] values();
public static com.test.Status valueOf(java.lang.String);
static {};
}
既然枚举类型是普通的类,那么我们也可以给枚举类型添加独有的成员方法
public enum Status {
RUNNING("睡觉"), STUDY("学习"), SLEEP("睡觉"); //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法!)
private final String name; //枚举的成员变量
Status(String name){ //覆盖原有构造方法(默认private,只能内部使用!)
this.name = name;
}
public String getName() { //获取封装的成员变量
return name;
}
}
public static void main(String[] args) {
Student student = new Student("小明", 18);
student.setStatus(Status.RUNNING);
System.out.println(student.getStatus().getName());
}
枚举类还自带一些继承下来的实用方法
Status.valueOf("") //将名称相同的字符串转换为枚举
Status.values() //快速获取所有的枚举
基本类型包装类
Java并不是纯面向对象的语言,虽然Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的。在学习泛型和集合之前,基本类型的包装类是一定要讲解的内容!
我们的基本类型,如果想通过对象的形式去使用他们,Java提供的基本类型包装类,使得Java能够更好的体现面向对象的思想,同时也使得基本类型能够支持对象操作!
- byte -> Byte
- boolean -> Boolean
- short -> Short
- char -> Character
- int -> Integer
- long -> Long
- float -> Float
- double -> Double
包装类实际上就行将我们的基本数据类型,封装成一个类(运用了封装的思想)
private final int value; //Integer内部其实本质还是存了一个基本类型的数据,但是我们不能直接操作
public Integer(int value) {
this.value = value;
}
现在我们操作的就是Integer对象而不是一个int基本类型了!
public static void main(String[] args) {
Integer i = 1; //包装类型可以直接接收对应类型的数据,并变为一个对象!
System.out.println(i + i); //包装类型可以直接被当做一个基本类型进行操作!
}
自动装箱和拆箱
那么为什么包装类型能直接使用一个具体值来赋值呢?其实依靠的是自动装箱和拆箱机制
Integer i = 1; //其实这里只是简写了而已
Integer i = Integer.valueOf(1); //编译后真正的样子
调用valueOf来生成一个Integer对象!
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) //注意,Java为了优化,有一个缓存机制,如果是在-128~127之间的数,会直接使用已经缓存好的对象,而不是再去创建新的!(面试常考)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); //返回一个新创建好的对象
}
而如果使用包装类来进行运算,或是赋值给一个基本类型变量,会进行自动拆箱:
public static void main(String[] args) {
Integer i = Integer.valueOf(1);
int a = i; //简写
int a = i.intValue(); //编译后实际的代码
long c = i.longValue(); //其他类型也有!
}
既然现在是包装类型了,那么我们还能使用==
来判断两个数是否相等吗?
public static void main(String[] args) {
Integer i1 = 28914;
Integer i2 = 28914;
System.out.println(i1 == i2); //实际上判断是两个对象是否为同一个对象(内存地址是否相同)
System.out.println(i1.equals(i2)); //这个才是真正的值判断!
}
注意IntegerCache带来的影响!
思考:下面这种情况结果会是什么?
public static void main(String[] args) {
Integer i1 = 28914;
Integer i2 = 28914;
System.out.println(i1+1 == i2+1);
}
在集合类的学习中,我们还会继续用到我们的包装类型!
面向对象编程实战
虽然我们学习了编程,但是我们不能一股脑的所有问题都照着编程的思维去解决,编程只是解决问题的一种手段,灵活的运用我们所学的知识,才是解决问题的最好办法!比如,求1到100所有数的和:
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 100; i++) { //for循环暴力求解,简单,但是效率似乎低了一些
sum += i;
}
System.out.println(sum);
}
public static void main(String[] args) {
System.out.println((1 + 100) * 50); //高斯求和公式,利用数学,瞬间计算结果!
}
说到最后,其实数学和逻辑思维才是解决问题的最终办法!
对象设计(面向对象、多态运用)
- 设计一个Person抽象类,包含吃饭运动学习三种行为,分为工人、学生、老师三种职业。
- 设计设计一个接口
考试
,只有老师和学生会考试。 - 设计一个方法,模拟让人类进入考场,要求只有会考试的人才能进入,并且考试。
二分搜索(搜索算法)
现在有一个有序数组(从小到大,数组长度 0 < n < 1000000)如何快速寻找我们想要的数在哪个位置,如果存在请返回下标,不存在返回-1
即可。
int[] arr = new int[]{1, 4, 5, 6, 7, 10, 12, 14, 20, 22, 26}; //测试用例
private static int test(int[] arr, int target){
//请在这里实现搜索算法
}
快速排序(排序算法、递归分治)
(开始之前先介绍一下递归!)快速排序其实是一种排序执行效率很高的排序算法,它利用分治法来对待排序序列进行分治排序,它的思想主要是通过一趟排序将待排记录分隔成独立的两部分,其中的一部分比关键字小,后面一部分比关键字大,然后再对这前后的两部分分别采用这种方式进行排序,通过递归的运算最终达到整个序列有序。
快速排序就像它的名字一样,快速!在极端情况下,会退化成冒泡排序!
0/1背包问题(回溯法、剪枝/动态规划优化)
给定 n
件物品,每一个物品的重量为 w[n]
,每个物品的价值为 v[n]
。现挑选物品放入背包中,假定背包能承受的最大重量为 capacity
,求装入物品的最大价值是多少?
int[] w = {2, 3, 4, 5};
int[] v = {3, 4, 5, 6};
int capacity = 8;
Java异常机制
在理想的情况下,我们的程序会按照我们的思路去运行,按理说是不会出现问题的,但是,代码实际编写后并不一定是完美的,可能会有我们没有考虑到的情况,如果这些情况能够正常得到一个错误的结果还好,但是如果直接导致程序运行出现问题了呢?
public static void main(String[] args) {
test(1, 0); //当b为0的时候,还能正常运行吗?
}
private static int test(int a, int b){
return a/b; //没有任何的判断而是直接做计算
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.test.Application.test(Application.java:9)
at com.test.Application.main(Application.java:5)
当程序运行出现我们没有考虑到的情况时,就有可能出现异常或是错误!
异常
我们在之前其实已经接触过一些异常了,比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception
类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!
运行时异常
异常的第一种类型是运行时异常,如上述的列子,在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常。所有的运行时异常都继承自RuntimeException
。
编译时异常
异常的另一种类型是编译时异常,编译时异常是明确会出现的异常,在编译阶段就需要进行处理的异常(捕获异常)如果不进行处理,将无法通过编译!默认继承自Exception
类的异常都是编译时异常。
File file = new File("my.txt");
file.createNewFile(); //要调用此方法,首先需要处理异常
错误
错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能JVM就无法继续正常运行了,比如OutOfMemoryError
就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)
int[] arr = new int[Integer.MAX_VALUE]; //能创建如此之大的数组吗?
运行后得到以下内容:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at com.test.Main.main(Main.java:14)
错误都继承自Error
类,一般情况下,程序中只能处理异常,错误是很难进行处理的,Error
和Execption
都继承自Throwable
类。当程序中出现错误或异常时又没有进行处理时,程序(当前线程)将终止运行:
int[] arr = new int[Integer.MAX_VALUE];
System.out.println("lbwnb"); //还能正常打印吗?
异常的处理
当程序没有按照我们想要的样子运行而出现异常时(默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息),我们希望能够自己处理出现的问题,让程序继续运行下去,就需要对异常进行捕获,比如:
int[] arr = new int[5];
arr[5] = 1; //我们需要处理这种情况,保证后面的代码正常运行!
System.out.println("lbwnb");
我们可以使用try
和catch
语句块来处理:
int[] arr = new int[5];
try{ //在try块中运行代码
arr[5] = 1; //当代码出现异常时,异常会被捕获,并在catch块中得到异常类型的对象
}catch (ArrayIndexOutOfBoundsException e){ //捕获的异常类型
System.out.println("程序运行出现异常!"); //出现异常时执行
}
//后面的代码会正常运行
System.out.println("lbwnb");
当异常被捕获后,就由我们自己进行处理(不再交给JVM处理),因此就不会导致程序终止运行。
我们可以通过使用e.printStackTrace()
来打印栈追踪信息,定位我们的异常出现位置:
java.lang.ArrayIndexOutOfBoundsException: 5
at com.test.Main.main(Main.java:7) //Main类的第7行出现问题
程序运行出现异常!
lbwnb
运行时异常在编译时可以不用捕获,但是编译时异常必须进行处理:
File file = new File("my.txt");
try {
file.createNewFile();
} catch (IOException e) { //捕获声明的异常类型
e.printStackTrace();
}
可以捕获到类型不止是Exception
的子类,只要是继承自Throwalbe
的类,都能被捕获,也就是说,Error
也能被捕获,但是不建议这样做,因为错误一般是虚拟机相关的问题,出现Error
应该从问题的根源去解决。
异常的抛出
当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就需要手动抛出一个异常来终止程序继续运行下去,同时告知上一级方法执行出现了问题:
public static void main(String[] args) {
try {
test(1, 0);
} catch (Exception e) { //捕获方法中会出现的异常
e.printStackTrace();
}
}
private static int test(int a, int b) throws Exception { //声明抛出的异常类型
if(b == 0) throw new Exception("0不能做除数!"); //创建异常对象并抛出异常
return a/b; //抛出异常会终止代码运行
}
通过throw
关键字抛出异常(抛出异常后,后面的代码不再执行)当程序运行到这一行时,就会终止执行,并出现一个异常。
如果方法中抛出了非运行时异常,但是不希望在此方法内处理,而是交给调用者来处理异常,就需要在方法定义后面显式声明抛出的异常类型!如果抛出的是运行时异常,则不需要在方法后面声明异常类型,调用时也无需捕获,但是出现异常时同样会导致程序终止(出现运行时异常同时未被捕获会默认交给JVM处理,也就是直接中止程序并在控制台打印栈追踪信息)
如果想要调用声明编译时异常的方法,但是依然不想去处理,可以同样的在方法上声明throws
来继续交给上一级处理。
public static void main(String[] args) throws Exception { //出现异常就再往上抛,而不是在此方法内处理
test(1, 0);
}
private static int test(int a, int b) throws Exception { //声明抛出的异常类型
if(b == 0) throw new Exception("0不能做除数!"); //创建异常对象并抛出异常
return a/b;
}
当main方法都声明抛出异常时,出现异常就由JVM进行处理,也就是默认的处理方式(直接中止程序并在控制台打印栈追踪信息)
异常只能被捕获一次,当异常捕获出现嵌套时,只会在最内层被捕获:
public static void main(String[] args) throws Exception {
try{
test(1, 0);
}catch (Exception e){
System.out.println("外层");
}
}
private static int test(int a, int b){
try{
if(b == 0) throw new Exception("0不能做除数!");
}catch (Exception e){
System.out.println("内层");
return 0;
}
return a/b;
}
自定义异常
JDK为我们已经提前定义了一些异常了,但是可能对我们来说不够,那么就需要自定义异常:
public class MyException extends Exception { //直接继承即可
}
public static void main(String[] args) throws MyException {
throw new MyException(); //直接使用
}
也可以使用父类的带描述的构造方法:
public class MyException extends Exception {
public MyException(String message){
super(message);
}
}
public static void main(String[] args) throws MyException {
throw new MyException("出现了自定义的错误");
}
捕获异常指定的类型,会捕获其所有子异常类型:
try {
throw new MyException("出现了自定义的错误");
} catch (Exception e) { //捕获父异常类型
System.out.println("捕获到异常");
}
多重异常捕获和finally关键字
当代码可能出现多种类型的异常时,我们希望能够分不同情况处理不同类型的异常,就可以使用多重异常捕获:
try {
//....
} catch (NullPointerException e) {
} catch (IndexOutOfBoundsException e){
} catch (RuntimeException e){
}
注意,类似于if-else if
的结构,父异常类型只能放在最后!
try {
//....
} catch (RuntimeException e){ //父类型在前,会将子类的也捕获
} catch (NullPointerException e) { //永远都不会被捕获
} catch (IndexOutOfBoundsException e){ //永远都不会被捕获
}
如果希望把这些异常放在一起进行处理:
try {
//....
} catch (NullPointerException | IndexOutOfBoundsException e) { //用|隔开每种类型即可
}
当我们希望,程序运行时,无论是否出现异常,都会在最后执行的任务,可以交给finally
语句块来处理:
try {
//....
}catch (Exception e){
}finally {
System.out.println("lbwnb"); //无论是否出现异常,都会在最后执行
}
try
语句块至少要配合catch
或finally
中的一个:
try {
int a = 10;
a /= 0;
}finally { //不捕获异常,程序会终止,但在最后依然会执行下面的内容
System.out.println("lbwnb");
}
思考:try
、catch
和finally
执行顺序:
private static int test(int a){
try{
return a;
}catch (Exception e){
return 0;
}finally {
a = a + 1;
}
}