第一章 走进java
1991年,Sun公司想要解决一个问题:希望设计一种计算机语言,这种语言主要用于消费类电子产品。而这类产品的特点是 计算处理能力和内存都有限且不同的厂家选择不同的CPU,这就要求这种语言必须足够小且紧凑,而且必须跨平台,最后将这种语言命名为Java。
计算机语言是人与计算机沟通的方式,计算机语言分为机器语言、汇编语言、高级语言。Java是面向对象的,更符合人的思维方式,且是在C++的基础上开发的,因此它是一种高级语言,并且相对C++更为简单,体现在不支持多继承和没有类似C++的指针的概念。Java语言除了简单性之外,还有面向对象(已提到过)、可移植性(一处编译、到处运行)、支持多线程、健壮性(有GC机制)以及安全性的特点。
Java语言是人与计算机沟通的语言方式之一,因为Java文件只有一份,而操作系统有多种且都不同,所以这直接决定了Java程序不能直接与操作系统打交道,由此引入了JVM(Java虚拟机)的概念,JVM是Java程序与OS的中间桥梁,Windows有Windows版本的JVM,Linux有Linux版本的JVM,所以不同OS的JVM是不同的。JVM是包含在JDK中的,JVM本质上是用C++写的软件。
当我们编写好java源代码时,java程序会经历编译阶段和运行阶段。编译阶段的目的是做校验,如果程序没有错误,则用户通过javac工具将.java文件编译成一个或多个.class文件,否则编译不通过;接着,用户通过java命令(如java A)触发JVM启动类加载器,类加载器去会去硬盘搜索A.class文件,找到后将该字节码文件加载到JVM中,JVM将.class解释成二进制数据,然后OS执行二进制文件与底层硬件进行交互。
详细说明参考:https://blog.csdn.net/weixin_42073629/article/details/86517147
现在,关于程序运行的过程,我举个例子说明以下,以求融会贯通:
首先,我们在记事本里编写一个名为MyTest01.java的源文件:
public class MyTest01 {
public static void main(String[] args) {
System.out.println("hello,world");
}
}
那么这个.java源文件并不能直接被操作系统所解析,我们需要JVM来帮我们解释成二进制文件,但是在此之前,需要先将.java源文件通过javac工具编译生成.class文件,继而我们再通过java命令触发JVM启动类加载器去硬盘中寻找这样的.class文件加载到JVM中,由JVM解释成二进制文件与计算机底层硬件进行交互。过程大概是这样的,但是现在的问题就是当我们用javac工具(这种程序是位于jdk的bin目录下的,这个目录同时还包含了java命令)在DOS窗口输入C:\Users\Simon>javac时,会提示“’javac’ 不是内部或外部命令,也不是可运行的程序或批处理文件。”,这是因为计算机找不到这种文件,因为计算机找某个文件的逻辑就是先在当前路径下找这种文件(注意:路径中”..”表示上级目录和”.”表示当前路径),找不到就会去C盘的用户目录下去找,再找不到就直接报错。当我们cd命令切换到java源文件所在目录时,用javac命令就不会报错,因为这是当前目录,如果我们要在任意路径输入javac命令都不报错的话,我们则需要去配置环境变量,由于环境变量包括用户变量和系统变量,用户变量仅作用于当前用户而系统变量是作用于所有用户,一般情况下个人电脑就选系统变量下的path,将jdk中bin目录的绝对路径粘贴到path中去,这就意味着我们键入bin目录中存在的指令时,这个指令都会在计算机搜索系统变量后被执行。所以,基于此,我们的.java文件会被编译成.class文件。(这里需要注意的是,命令格式是javac 路径,这里是包含.java后缀的,因为要编译的是一个文件)。接着.class文件会被类加载器加载到JVM中,这是由java命令触发的,java命令的格式是“java 类名”,是不带.class的,因为运行的是一个类,不是一个文件,更不是一个路径,所以就和我们编辑的类名相同。
在通常写代码时,特别是开发一个较为庞大的项目时,为了程序的可读性,我们往往会在代码里添加一些注释,注释的本质是为了方便人的阅读,在编译时会忽略注释,原样保留到.class文件中,这些注释分为三种,单行注释、多行注释和文档注释。格式如下:
//单行注释
/
多行注释1
多行注释2
/
/*
文档注释1
文档注释2
/
值得注意的是,jdk的bin目录下的javadoc命令可以参与生成文档注释,这就起购买电脑时会有一本说明书的作用,就是一个帮助文档。
在java文件中,我们编写如下几个类:
class A{}
class B{}
class C{}
public class MyTest02{
public static void main(String[] args){
System.out.println("My Test02.");
}
}
class MyTest{
public static void main(String[] args){
System.out.println("My Test.");
}
}
可以发现,一个java文件中有多个类,但是最多只能有一个public类,且如果存在public类,那么java文件名必须与此类名相同。对以上代码进行编译如下:
(前)
(后)
这时,所有类都被编译生成了.class文件,如果要单独运行某个类,直接使用类似 “java MyTest”这样的命令,但是如果键入“java A”则会报错,因为A没有main()方法,而main()方法是执行程序的入口,即编译期不会报错,因为格式正确;运行会报错,因为没有程序执行的入口。正因为每一个类方法都可以编写main()方法,所以显得java语言更加灵活。
本章重点内容要求如下:
- 理解java的加载与执行
- 能够自己搭建Java的开发环境
- 能够独立编写类似MyTest程序,并成功编写与运行
- 明白环境变量path的原理以及如何配置
- 掌握环境变量classpath的原理以及如何配置
- java中的三种注释
- public class 和 class的区别
第二章 Java语言基础
Java语言非常丰富,构成Java语言基础的包括标识符、关键字、数据类型、进制转换、字符编码、变量、运算符、控制语句、方法等。
标识符,顾名思义,就是用来标识的符号,这类符号可以作用于接口、类、方法、变量和常量。在日常开发过程中,凡是能通过编译的名称都能称为标识符,标识符由字母、数字、下划线和美元符号,除了不能用数字作为开头,其他三种都可以作为开头。这里,接口和类遵从大驼峰命名规范,方法和变量遵从小驼峰命名规范,只有常量全用大写字母命名且若含有几个单词构成的常量,往往用下划线连接。
Java关键字就是通常的那40多个,这里参见百度百科的:https://baike.baidu.com/item/java%E5%85%B3%E9%94%AE%E5%AD%97/5808816。
数据类型,本质上是将数据进行类型划分,由于我们可以将世间万物看成是由数据构成的,而不同的数据在内存中存储的大小也是不同的,为了方便区分,我们将数据分成了两大类:基本数据类型和引用数据类型,基本数据类型分为四类八种,即:数值型数据类型—-byte、short、int、long,浮点型数据类型:float、double,布尔型数据类型:boolean,字符型数据类型:char。计算机为数据开辟空间大小的依据就是数据类型,比如:byte是1个字节大小即8位,boolean是1个字节,char是1个字节,short是2个字节,int为4,float为4(这个4与int类型的4是有区别的),long为8,double为8(这个与long的8个字节也是有区别的)。计算机为变量开辟空间时,就按这个进行空间划分,由于变量的定义是如:int age = 25,即:数据类型 变量名 = 变量值大小 ,所以数据类型最本质的作用是指导计算机为数据分配什么样的内存、多大的内存。注意,数据类型之间可能会存在转换,比如byte hs = 17,因为整数默认为是int类型,实际上这里发生了强转,即byte hs = (byte)17,但后面的jdk做了优化,在byte(-128—-127)数字表示的范围内自动转为byte型,比如short st = 99就不会报错,因为99默认为int型后,会自动转为short型,强转的原理就是砍掉高位保留低位,如byte hs = 17,17的int型二进制为: 00000000 00000000 00000000 00010001,转成byte后就直接变成了00010001,因为将前面3个字节全部砍掉,所以若为负数获取其他非0的数,可能会存在精度丢失的情况。总之,在各自数值表示的合法范围内,不需要手动强转。如:
浮点型数据类型包含了单精度浮点型float和双精度浮点型double,一般用于表示相对精确的数据,然而,在类似财务的问题上,浮点数标识符仍然不够精确,他们只是存储的近似值,这时我们可以使用java.math.BigDecimal包下的BigDecimal类用于表示精确的数据。
在日常开发中,经常会遇到基本数据类型相互转换的问题,关于基本数据类型的相互转换,有以下几点值得注意:
- 除了boolean类型的数据不能与其他数据转换之外,其他都能互相转换,如果是小容量转换成大容量(如short转成int),则无须手动干预,计算机会自动转换;反之则需要强制类型转换
- byte、short、char进行混合运算的时候,会转成容量最大的那种类型,这里是int;如果是浮点型就直接先转成double,其实也就是默认double
- 经典的面试题:①byte h=(byte)(int)g/3,编译不通过,只是因为语法检查;byte h=(byte)(int)(g/3),编译通过 ②short i = 10,byte j=5,short k = i+j,编译不通过
变量,顾名思义,就是会变化的量,使用变量时要求先声明、赋初值再使用,否则就会报错:Variable ‘i’ might not have been initialized。变量主要分为局部变量和成员变量,局部变量就是存在于方法种的变量,成员变量是直接定义在类体中的变量,这个变量与方法是同级的。
进制转换,之所以存在进制转换,是因为人习惯的是十进制,而计算机只能接受二进制语言,所以人与计算机要想交流就存在进制转换的问题。有时候为了方便生活,人们也发明了八进制和十六进制,这些进制之间的转换与计算机组成原理密切相关,会涉及到一些概念如何原码、反码、补码等,此处不方便赘述。
字符编码,是建立在进制转换的基础之上的,字符编码的本质就是将我们的自然语言如英语、汉语、法语、德语、俄语等映射成二进制数,所以说,字符编码本质上就是一种自然语言与机器语言的映射。首先是美国人发明的ASCII码,这个码包括26个字母的大小写、英语符号、控制字符等,总计不超过128个;随着需求的增加,世界各国人民也想用本国语言与计算机打交道,于是也采用类似的方式有了许多种映射表,包括GB2312、GBK、GB18030、big5等,由于语言之间的切换比较困难,后来出现了Unicode,这套字符集囊括了世界主要语言,并且这套体系下有许多种规范,其中使用最为广泛的是UTF-8。我们把二进制语言转换为自然语言的过程称为解码,将自然语言转换为二进制语言的过程称为编码。开发中出现的乱码问题,本质上是因为编解码不一致。值得注意的是,在我们编程过程中,局部变量必须赋初始值,否则编译不通过;而成员变量可以不用赋初值,且8种数据类型的默认找向0看齐,如byte、short、int、long默认初值都是0,float、double都是0.0,char默认\u0000(这里需要注意的是,\u是指转义成unicode的意思,因为char的默认值其实还是0),boolean默认初值是false(java中false=0,true=1)。
转义字符,上文提到的转义字符是为了让字符变成具有特定功能的符号如:”\n”、”\t”、”\r”分别表示换行、空出一个tab位置、”回车”,但是如果要表示一个点,一条反斜杠就不能直接用”\”,而应该分别是”.“、”\“。值得注意的是,我们可以通过jdk的bin目录下native2ascii.exe将中文转成unicode编码形式 ,键入“我是一个中国人”时,回车的结果的ASCII码为:\u6211\u662f\u4e00\u4e2a\u4e2d\u56fd\u4eba,这就是表示6211等数字不是表示字符或者字符串,而是具有特定意义的数字。
关于运算符(算术、关系、逻辑、赋值、字符串连接符、三元)、流程控制语句(关键字if、switch、for、while、do…while、break、continue等),此处不做说明和讲解。
方法是完成某一特定功能的代码段,是由若干Java语句组成的。其语法是:
【修饰符列表】 返回值类型 方法名(形参列表){
方法体;
return 返回值;
}
这里,修饰符列表_;返回值类型就是数据类型,可以是基本数据类型,也可以是引用数据类型,甚至是空;方法名就是这个方法的名称,一般是由动词组成;形参列表是形式参数的列表,也叫入参,可以为空;方法体就是方法的核心语句;如果返回值类型为空,则可以使用“return;”这样的格式,当然也可以为空,如果不为空,就必须返回与返回值类型相同的数据类型。当修饰符为static时,要用“类名.方法()”调用,但如果是本类中的方法,则可以省略不写。在实际开发中,一般是一个java文件对应一个java类。当一个方法有返回值且我们调用了方法时,对于调用者来讲,可以选择接收,也可以选择不接收,但大部分情况下我们是要接收的,使代码显得美观、便于阅读。我们写代码时,在同一方法体中应避免return后继续写代码,因为return返回的是整个方法,且return要保证有100%的返回。
第三章 面向对象
Java是一种纯面向对象的语言,与面向过程的C语言是不一样的。面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
面向对象的主要内容是类和对象。可以将对象理解为生活中常见的某一单个实物,而类就是对这些实物的抽象,所以可以说类是一个个对象的抽象,对象是类的具体体现。比如,猫是一个类,黑猫白猫就是对象;反之,大熊猫和老虎都是猫科动物的抽象。
类的语法结构如下:
【修饰符】 class 类名{
属性;
方法;
}
而对象是通过“new 类名();”创建的,还有一种情况就是String name = “张三”;这里name其实也是对象。我们可以通过一段代码和JVM内存模型图来描述类、对象、方法的具体执行过程:
public class MyTest1 {
public static void main(String[] args) {
MyMath mm = new MyMath();
//方式一:不用匿名内部类:接口、实现类、调用接口方法、测试类输出
mm.mySum(new ComputeImpl(),100,200); //100+200=300
}
}
interface Compute{
int sum(int x,int y);
}
class ComputeImpl implements Compute{
@Override
public int sum(int x, int y) {
return x+y;
}
}
class MyMath{
public void mySum(Compute compute , int x, int y){
int retValue = compute.sum(x, y);
System.out.println(x + "+" + y + "=" + retValue);
}
}
整个过程执行如下:
首先,JVM内存结构包括堆、栈、方法区等,堆和方法区只有一个,而一个线程对应一个栈内存。当我们编写完一段代码后,通过“javac 类名.java”编译生成class文件,再通过“java 类名”触发JVM启动类加载器去硬盘里面加载相应的class文件到JVM中的方法区,所以JVM运行时数据区之一的方法区就用来存放.class文件,计算机扫描后会将Person、Computer和String类加载到方法区中。由于main()方法是程序执行的唯一入口,所以JVM从main()方法中进去后,依次执行Java语句。“new Person(); ”的意思是在堆内存区开辟一块内存空间,并初始化其中的几个属性,此时id=0,name=null,computer=null,然后将Person对象的地址赋值给位于栈内存的person,也就是Person类型的person变量指向堆内存的Person对象,注意person既是一个局部变量,也是一个引用,我们常见的空指针异常就是在本例中的体现就是person指向了空,所以空引用访问了实例相关的数据就会报空指针异常,这是因为编译能通过但运行不通过;接着,同样在堆内存区开辟了内存空间,取名为Computer,Computer类型的computer指向了这样的内存空间,类似的,Computer的对象也会初始化类中的2个属性,类似的,brand是String类型的变量,不能直接初始化,还要重新开辟一块String类型的空间;第三条语句“person.computer = new Computer();”其实就是将堆内存的computer变量指向Computer对象,第四条语句“person.computer.brand = “Huawei” ;”就是将”Huawei”赋值给computer的brand变量,最后打印输出person.computer.brand 和 person.name。方法执行结束。
每种编程语言都有自己的特点,Java语言也不例外。封装、继承、多态是Java语言的三大特点,所谓封装,就是为了防止外界通过构造方法调用内部属性的时候做任意修改,这会造成数据不安全,所以,有必要将内部的属性访问权限设置为private,即只有本类成员才可以访问。所以外界可以通过本类的其他成员访问属性值并做修改,而这里的其他成员一般是指getter和setter方法,在这两种方法内部做一些逻辑判断,比如设置User类age属性为0-150,防止外界乱作修改,setter方法可写成“if(age>=0&&age<=150)”然后做修改操作,否则退出方法。所谓继承,就是extends,当一个类与另外一个类发生了一些联系,一个类有另一个类都有的东西,那么,可以将这个类设为子类,另一个设为父类,共有的属性、方法不必再写,只需要用一个extends来表达两者的关系。这种现象就是继承,有了继承,才有了方法的覆盖和多态机制,不过与C++不同,Java只支持单继承。这里的继承,除了私有和构造相关的代码,其他都支持。多态可以理解为多种形态,指的是编译期形态和运行时形态,包含向上转型和向下转型两个内容。比如:Animal a2 = new Cat();读作:猫是一种动物。自动类型转换:父引用指向子类对象。其语法特征是,编译看父类、运行看子类。值得注意的是,两个类之间必须要有继承关系,否则向下转型时会有类型转换异常,如何避免:instanceof运算符,语法格式:引用 instanceof 数据类型名。为什么要类型转换赋值呢?因为要降低程序的耦合度,提高程序扩展力,能使用多态就尽量使用多态。其核心是,面向对象编程,尽量不要面向具体编程。
构造方法称构造器,构造器有2个作用,①创建对象 ②初始化内存空间的地址。首先引出方法的调用,一般如果我们的方法是通过static修饰(类方法)的话,调用这个方法会用“类名.方法”;如果没有static修饰(实例方法)的话,可以采用“引用.方法”调用。但实际上,调用方法还有一种方式,就是通过new关键字,一般意义上,new就是在堆内存上开辟一块内存空间,可以这样理解,其实还有一种理解方式—“new 构造器()”,然后将内存空间的地址赋值给名字与构造器相同的类的引用,只是这里我们在方法的语法上省去了return关键字,返回值类型依然还是构造器所在类的类型。不然可以试试“new User()”,编译运行依然能通过。所以,对象是怎么来的?并不是通过new关键字,而是通过调用构造方法创建的。构造方法作用之一是创建对象,另一个作用是初始化变量的内存空间,如果我们没有手动写构造器,系统会为我们自动创建一个无参构造器,无参构造器会也会初始化实例变量的内存空间。构造器有多个,这里体现了Java语言多态的特点。
package test001;
/**
* 构造方法的作用:
* ①创建对象(通过new关键字调用构造方法)
* ②对象初始化,即初始化实例变量的内存空间
*/
public class ConstructorTest01 {
public static void main(String[] args) {
//默认User的构造方法
new User(); //构造方法执行之后其实是有返回值的,只不过返回值类型就是构造方法所在类的类本身,
// 所以构造方法看起来没有返回值,不写return;
//默认User的构造方法
User user1 = new User();
System.out.println("id:" + user1.getId() + ",name:" + user1.getName());//id:0,name:null
}
}
package test001;
public class User {
private String name;
private int id;
public User(){//缺省构造器
System.out.println("默认User的构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
倘若我们在一个类中定义了一个main方法和一个不带static的实例方法,然后在main中调用了这个实例方法,一般语法格式直接就是“实例方法名();”,其实,本质上讲,真正的语法格式是“this.实例方法名”,this表示的是“始终指向当前对象”,比如:“User user = new User();”这一句中,user作为引用,始终指向在堆中的User的内存地址,user是在栈中的,它保存的信息就是User的内存地址,而this也有这个作用,唯一不同的是,this是在堆中的User的内部,始终指向当前类的对象。哪个对象调用了当前类的成员,this就指向哪个对象。所以,每创建一个对象,就有一个this引用。实例方法是属于对象级别的,不同的对象会导致方法的行为不同,这是判断标准,通过“引用.”调用。name = this.name,这里取等价的意思。以上是this的第一个作用:即始终指向当前类的对象。(一般情况下get和set方法不会省略this,其他情况通常会省略写this)。第二个作用是,调用别的构造器。语法格式:this(实参);,要求必须出现在构造方法调用者的第一行。比如,有一个类名为Date(),有一个带全参的构造器和一个无参构造器,全参构造器初始化了属性值,而无参构造器也想用这些Java语句,那么就可以直接用“this.Date(2008,8,8)”。一般而言,在main中调用静态方法是“类名.方法”,但是,如果用“对象.方法”其实也是可以的,可以做个实验,将对象赋值为null,然后用“对象名.方法名”去调,并不会出现空指针异常,相反,还是和“类名.方法名”是一样的输出结果。其实,本质上“对象名.方法名”就是“类名.方法名”,但是为了规范和增强代码的可读性,这种情况下建议使用“类名.方法名”调用。
————————————————————————————————————————————————————————
————————————————————————————————————————————————————————
package test001.thisdemo;
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer();//等号右边执行完成了之后,实际上this已经保存了地址指向了自己
c1.name = "zhangsan";
Customer c2 = new Customer();
c2.name = "lisi";
}
}
package test001.thisdemo;
public class Customer {
String name;
public Customer() {
}
}
========================================================================
“this.”的作用:
package test001.thisdemo;
public class Customer {
String name;
public Customer() {
}
public void shopping(){
//这里this.可省略不写
System.out.println(this.name + "在购物");//c1去访问,this就是c1,c2去访问,this就是c2
}
}
package test001.thisdemo;
public class CustomerTest {
public static void main(String[] args) {
Customer c1 = new Customer();//等号右边执行完成了之后,实际上this已经保存了地址指向了自己
c1.name = "zhangsan";
c1.shopping();
Customer c2 = new Customer();
c2.name = "lisi";
c2.shopping();
}
}
package demo2;
public class Test {
public static void main(String[] args) {
Test.doSome(); //do some
Test test = new Test();
test.doSome(); //do some
test = null;
test.doSome(); //do some,并没有出现空指针异常
}
public static void doSome(){
System.out.println("do some");
}
}
Java根据代码执行阶段的不同,分为编译期和运行期。当java工具触发类加载器将.class文件从硬盘中加载到方法区时,有一些变量就已经被分配内存且初始化了,这些变量是由static修饰的,是属于类级别的。需要调用这些变量或者方法时,需要用“类名.变量”或“类名.方法”的格式。当变量或者方法不会因为是哪个对象而产生不同的结果时,往往可以使用static修饰,叫做静态成员,表示所有对象共享一个。静态代码块是一段Java语句,外部由static修饰,用大括号括起来,而示例代码块只是由一对大括号括起来的,静态代码块可以编写多个,且类加载时只加载一次,产生静态代码块的目的一般是为了记录日志,日常开发中,我们可以发现,工具类方法一般是静态方法,因为不用再创建一个对象,方便省事;而实例代码块是对象初始化、构造器执行之前执行的,可以执行多次,因为每产生一个对象就会执行一次实例代码块。
当子类和父类扩能类似,但是父类方法中的一些Java语句无法满足子类需求,这是需要对父类方法做一些修改以满足子类方法的需求,这种现象叫做重写或者覆盖。为防止写错,用@overwrite注解写在方法之前,Java程序编译的时候就能识别语法是否与父类保持一致。需要注意的是,子类方法重写后访问权限不能更低、抛出异常不能更多。静态方法不支持重写,是因为父子类共享同一份数据,无法让子类对父类静态方法重写。覆盖只针对方法、不谈属性。
对于一个类、方法或者变量,当我们不想让子类对其做出修改的时候,可以对方法或者属性冠以final修饰final可用在方法中,但不可在类成员位置声明final,除非对其初始化,如:final int age错,final int age=10对。常量的语法格式:public static final 常量名 = 值;
package表示包的意思,语法格式就是在类的第一行写上“import package 包名”。关于访问控制权限的问题,涉及到数据安全,所以Java特意创造了几个访问权限修饰符:private、protected、public和缺省,其主要内容就是private只能是在本类中访问成员,缺省是只能在同包下访问,protected是可以在同包下、不同包的子类下访问,public可以在任意位置访问相当于不设防。因此,访问权限由低到高是:private、缺省、protected、public。值得注意的是,普通类(相对于内部类)只能由public和缺省修饰。
存在继承关系的父子类之间,通常子类继承父类的属性和方法之后,自己会有一些特有的属性或方法。而子类对象的父类特征,通常用“super”来表示。比如子类要调用父类的属性语法格式是“super.属性”,子类调用父类的方法语法格式是“super.方法”,子类调用父类的构造方法的语法格式是“super(实参)”。了解到这一层其实就够了,但如果能理解其JVM内部特征的话就算真的掌握了。
public class Custom{
String name;
//无参构造方法
//带一个参数的构造方法
}
public class Vip extends Custom{
String name;
}
psvm(String[ ] args){
Vip v = new Vip();
this.name.sout;
super.name.sout;
name.sout;
}
其实,每创建一个对象,都会调用构造方法的时候都会先去调用父类的构造方法,直到Object为止。由于this始终指向的是当前对象,super是用来初始化对象的父类特征的,所以,super并不是用来创建对象的,也就是说,JVM看起来调用了父类的属性和行为,但实际上那都是子类的,并没有创建别的对象。当父子类有重名的属性或者方法的时候,super关键字不能省略,this.xxx和super.xxx并不是同一个属性或者行为,其实他们是在不同的内存地址的,不是简单的覆盖关系,而是在两个区域。有一点要注意,与this不同的是,super不是引用,不保存内存地址,也不指向任何对象。我个人觉得,super本质上就是一块内存区域,这个区域用来存放具有父类特征的数据。
package test001.superDemo;
public class SuperTest02 {
public void doSome(){
System.out.println(this); //test001.superDemo.SuperTest02@677327b6
System.out.println(super.);//Error:(6, 34) java: 需要<标识符>
//以上输出可以说明,this是引用。
// 而super必须加个点才能编译通过,说明不是引用,即不能单独使用,不保存内存地址,也不指向任何对象,
// 只是代表当前对象内部的那一块父类型的特征
}
public static void main(String[] args) {
SuperTest02 st2 = new SuperTest02();
st2.doSome();
}
}
package test001.superDemo;
public class SuperTest01 {
//this能出现在实例方法、构造方法中,不能出现在静态方法中
//this在区分局部变量和实例变量的时候不能省略
public static void main(String[] args) {
new B();
}
}
class A{
public A(){
System.out.print("A的无参构造。。。"+"\t");
}
public A(int a){
}
}
class B extends A{
public B(){
// super(100); //B的无参构造...
//若无super(100),则输出:A的无参构造。。。 B的无参构造...
System.out.println("B的无参构造...");
}
}
抽取多个对象的共同点的过程叫抽象,这样的类称为抽象类;相应的,从类到具体的对象这个过程叫实例化,这样的对象可以称为实例。事实上,多个类依然有共同点,还可以继续抽象,他们的父类可以称为抽象类,抽象类仍然可以继续抽象,成为接口。而从接口到具体的对象,就是实例化的过程。但是这样的描述显得笼统而模糊,先解释一下抽象类和接口的区别:①抽象类是类的抽象,它依然属于类的范畴,只是其内部必须包含抽象方法—-没有实现体的方法,可以有非抽象方法;也就是说,抽象方法必须存在于抽象类里,抽象类除了抽象方法还可以有别的。 ②抽象类含有构造方法,这个是供子类继承的,不是用来实例化对象的,因为子类继承抽象类时要先调用父类的构造方法,且必须实现其抽象方法。日常开发中,我们尽量面向抽象编程,不要面向具体编程,应用多态的思想,编译时看父类运行时看子类 ③Java语言中,有没有方法体不是区分抽象方法和非抽象方法的重要标志,因为C++写的native方法是没有方法体的,该关键字表示调用的是JVM本地程序。 ④接口是堆抽象类的抽象,也称完全抽象,所以方法中只有抽象方法,修饰符public abstract可省略,都是公开的;常量public static final可省略 ⑤接口是支持多继承的,所以之间没有继承关系时也能强转,但运行时可能会出现类转换异常,但编译不会报错 ⑥关于is-a、has-a和like-a的关系:Animal animal = new Cat();本质上就是“Cat is an Animal”的关系,即继承;has-a可以简单理解为一个类作为另一个类的属性存在形式,比如Person是个类,Student的属性就包含Person person,除此之外还有int age,String name之类的;like-a就是类实现了某个接口,就说类like该接口。接口的好处在于,基本轮子造好了,也实现调用者和实现者之间的解耦合,往往只需要改动测试类的代码就能实现不一样的功能。
以上是关于Java基础语法的知识,但在实际开发中,我们用的更多的是调用API,即调用Sun公司写好的应用程序接口。所有类的根类是Object类,Object类有以下方法:
protected Object clone(); //创建并返回此对象的一个副本,有深浅克隆之分
boolean equals(Object obj); //判断两个对象是否相等
protected void finalize(); //系统调用的垃圾回收操作
Class<?> getClass(); //返回此Object的运行时类
int hashCode(); //返回该对象的哈希值码
void notify(); //唤醒在此对象监视器上等待的单个线程
void notifyAll(); //唤醒在此对象监视器上等待的所有线程
String toString(); //返回该对象的字符串表示形式
void wait(); //在其他线程调用此对象的 notify()) 方法或 notifyAll()) 方法前,导致当前线程等待
void wait(long timeout); //在其他线程调用此对象的 notify()) 方法或 notifyAll()) 方法,或者超过指定的时间量前,导致当前线程等待。
void wait)(long timeout, int nanos); //在其他线程调用此对象的 notify()) 方法或 notifyAll()) 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
其中,有几个方法值得详细了解一下。
toString()方法的默认实现是类名@16进制地址,其作用是将对象转成字符串形式,为了增加程序的阅读性,子类一般要重写toString()方法。但是,SUn公司已经实现了当我们调用println(引用)的时候,引用自动调用toString()方法。
equals方法:默认判断条件this==obj,判断两个对象是否相等;equals采用默认的“==”来比较对象,其实应该用对象值去做比较。往往要判断两个对象是不是同一类型,要用instanceof判断,然后强转,调用属性去比较。所以,基本数据类型用==,引用数据类型用equals判断。值得注意的是,重写equals就要重写彻底;toString()方法已经重写了equals方法。以后写代码,先来几个判断语句,最后比较;而不是所有情况都先执行一段代码再比较。
protected finalize(),这个方法是由GC负责调用,需要重写。Person类重写finalize();,new Person()然后将引用赋值为null时会调用。一般去情况下,时间到没到、垃圾够不够多就不会触发GC回收机制,在写demo的时候可能看不到效果。
public native int hashCode()方法,是对Java对象的内存做了一些操作后得到的值。
类与类之间的关系分两种,一种是相互平行,另一种是内部与外部的关系。内部类就是这样的关系。内部类根据位置可分为三种:静态内部类、实例内部类和局部内部类,其中,局部内部类包括了匿名内部类。匿名内部类是使用最多的内部类。现在,做个测试,来体现内部类的用处:
方式一:不使用内部类:
package test001.anonamousdemo;
public class AnonamyousTest {
public static void main(String[] args) {
MyMath mm = new MyMath();
mm.mySum(new ComputeImpl(),20,30);//50
}
}
interface Compute{
int sum(int x,int y);
}
class ComputeImpl implements Compute {
@Override
public int sum(int x, int y) {
return x + y;
}
}
class MyMath{
public void mySum(Compute compute,int x,int y){
int sumRet = compute.sum(x, y);
System.out.println(sumRet);
}
}
方式二:使用内部类:
package test001.anonamousdemo2;
public class AnonamyousTest {
public static void main(String[] args) {
MyMath mm = new MyMath();
//一般来说,接口和抽象类都是不能new的,但是这里使用匿名内部类,
//实质是Compute compute =new ComputeImpl();new Compute(){}的大括号表示对这个接口的实现
mm.mySum(new Compute() {
@Override
public int sum(int x, int y) {
return x+y;
}
},20,30);//50
}
}
interface Compute{
int sum(int x, int y);
}
class MyMath{
public void mySum(Compute compute, int x, int y){
int sumRet = compute.sum(x, y);
System.out.println(sumRet);
}
}
附:一个酒店管理系统小项目
1、写一个Room类,表示房间,包含房间的编号、类型以及是否空闲
package HotelMgt;
import java.util.Objects;
public class Room {
/**
* 房间号,例如101109,201209,etc
*/
private int no;
/**
* 房间类型,例如豪华套房、总统套房、普通房间、标准单间,etc
*/
private String type;
/**
* 是否可预定,true表示空闲,false表示占用
*/
private boolean order;
public Room(int no, String type, boolean order) {
this.no = no;
this.type = type;
this.order = order;
}
public Room() {
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public boolean getOrder() {
return order;
}
public void setOrder(boolean order) {
this.order = order;
}
//equals和toString方法
public boolean equals(Object obj){
if (obj == null||!(obj instanceof Room)) return false;
if (obj == this) return true;
Room o = (Room)obj;
if (this.no ==o.getNo()&&this.type.equals(o.getType())&&this.order==o.getOrder()) return true;
return false;
}
public String toString(){
return "["+no+","+type+","+(order?"空闲":"占用")+"]";
}
//供本类测试方法之用
public static void main(String[] args) {
Room room1 = new Room(101, "总统套房", false);
Room room2 = new Room(1011, "总统套房", false);
System.out.println(room1.equals(room2)); //false
}
}
2、写一个管理系统类,包括Room类型的引用,一个打印方法、一个退房方法和一个订房的方法
package HotelMgt;
public class HotelMgt {
/**
* 酒店是有若干个房间组成的
*/
private Room rooms[][];
public HotelMgt() {
rooms = new Room[4][10]; //4层楼,每层楼10个房间
for (int i = 0; i < rooms.length; i++) {
for (int j = 0; j < rooms[i].length; j++) {
if (i == 0){
rooms[i][j] = new Room((i + 1) * 100 + j, "标准单间", false);
}else if (i == 1){
rooms[i][j] = new Room((i + 1) * 100 + j, "普通单间", true);
}else if (i == 2){
rooms[i][j] = new Room((i + 1) * 100 + j, "豪华套房", false);
}else if (i == 3){
rooms[i][j] = new Room((i + 1) * 100 + j, "总统套房", true);
}
}
}
}
//提供一个打印酒店的方法
public void printHotel(){
for (int i = 0; i < rooms.length; i++) {
for (int j = 0; j < rooms[i].length; j++) {
Room room = rooms[i][j];
System.out.print(room);
}
System.out.println("\n");
}
}
//订房方法
public void orderRoom(int roomNo){
//比如206对应的是room[1][5]
Room room = rooms[roomNo/100-1][roomNo%100-1];
room.setOrder(false);
System.out.println(room.getNo()+","+room.getOrder()+","+"成功订房");
}
//退房方法
public void exitRoom(int roomNo){
//比如206对应的是room[1][5]
Room room = rooms[roomNo/100-1][roomNo%100-1];
room.setOrder(true);
System.out.println(room.getNo()+","+room.getOrder()+","+"成功退房");
}
}
3、一个测试类
package HotelMgt;
public class HotelTest {
public static void main(String[] args) {
HotelMgt hotelMgt = new HotelMgt();
hotelMgt.printHotel();
hotelMgt.orderRoom(203);
hotelMgt.exitRoom(105);
}
}
实现效果:
番外:IDEA工具的使用
相关的快捷键:https://m.php.cn/java/guide/478876.html详见
psvm、sout、自动保存
删除一行,ctrl+Y
新建:Alt+insert(包括新建类、接口和getter、setter)
窗口大小变换:ctrl+shift+F12
打开IDEA界面0-7,Alt+数字
ctrl+p 查看方法的参数信息
ctrl+shift+alt:多行操作
psvm:生成main()方法;
fori:生成for循环;
Ctrl+Alt+v:自动补齐返回值类型
ctrl+o:覆写方法
ctrl+i:实现接口中的方法
ctrl+shift+u:大小写转换
CTRL+SHIFT+Z:取消撤销
Alt+Insert:生成构造方法、getter、setter
ctrl+y:删除当前行
Ctrl+Shift+J:将选中的行合并成一行
ctrl+g:定位到某一行
Ctrl+Shitft+向下箭头:将光标所在的代码块向下整体移动
Ctrl+Shift+向上箭头:将光标所在的代码块向上整体移动
Alt+Shift+向下箭头:将行向下移动
Alt+Shift+向上箭头:将行向上移动
Ctrl+F:在当前文件中查找
Ctrl+R:替换字符串
Ctrl+Shift+F:在全局文件中查找字符串
Ctrl+Shift+R:在全局中替换字符串
Ctrl+Shift+Enter:自动补齐{}或者分号;
Shift+Enter:在当前行的下方开始新行
Ctrl+Alt+Enter:在当前行的上方插入新行
Ctrl+Delete:删除光标所在至单词结尾处的所有字符
第四章 Java进阶
4.1String类、StringBuffer类和StringBuilder类
4.1.1 String类的构造方法
String s1 = "java String test1...";
String s2 = s1 + "java String test2...";
String s3 = "java String test1...java String test2...";
String s4 = new String(s3);
System.out.println(s2 == s3); //false
System.out.println(s2.equals(s3)); //true
System.out.println(s3 == s4); //false
System.out.println(s3.equals(s4)); //true
byte[] bytes = {97,98,99};
String s5 = new String(bytes);
String s6 = new String(bytes, 0, 1);
System.out.println(s5); //abc
System.out.println(s6); //a
char[] chars = {'我','是','中','国','人'};
String s7 = new String(chars);
String s8 = new String(chars, 2, 2);
System.out.println(s7); //我是中国人
System.out.println(s8); //中国
4.1.2 String类的成员方法
//注意:equals()和compareTo()的区别是,前者只能看出是否相等;后者还能比大小
System.out.println("java".compareTo("javac")); //-1
System.out.println("javac".compareTo("java")); //1
System.out.println("java".compareTo("java")); //0
System.out.println("my java test01".contains("java")); //true
System.out.println("my java test02".endsWith("02")); //true
System.out.println("my JAVA test03".equalsIgnoreCase("my java test03")); //true
//将字符串对象转换成字节数组
//109 121 32 106 97 118 97 32 116 101 115 116 48 52
byte[] bytes = "my java test04".getBytes();
for (int i = 0; i < bytes.length; i++) {
System.out.print(bytes[i]+"\t");
}
System.out.println("my java test05".indexOf("a")); //4
System.out.println("my java test06".lastIndexOf("a")); //6
System.out.println("my java test07".isEmpty()); //false
System.out.println("".isEmpty()); //true
//my java String test08
System.out.println("my java test08".replace("java", "java String"));
//split()的作用是用特定格式分割字符串
//https: www.bilibili.com video BV1Rx411876f?p=599 true
String[] s7 = "https://www.bilibili.com/video/BV1Rx411876f?p=599".split("/");
for (int i = 0; i < s7.length; i++) {
System.out.print(s7[i]+" ");
}
System.out.println("my java test10".startsWith("m")); //true
System.out.println("my java test11".substring(1, 12)); //y java test
char[] s8 = "my java test12".toCharArray();
for (int i = 0; i < s8.length; i++) {
System.out.print(s8[i]+" "); //m y j a v a t e s t 1 2
}
System.out.println("MY JAVA tEST13".toLowerCase()); //my java test13
System.out.println("my java test14".toUpperCase()); //MY JAVA TEST14
//Hello,World! My java test15
System.out.println(" Hello,World! My java test15 ".trim());
//valueOf()是静态方法,作用是将参数内的一切类型转换成字符串类型
System.out.println(String.valueOf(3.1415926)); //3.1415926
System.out.println(String.valueOf('z')); //z
System.out.println(String.valueOf(false)); //false
4.1.3 StringBuffer和StringBuilder
String本质上是由final修饰的字符数组或者字节数组,因此当我们需要大量字符拼接的话,JVM就会在方法区开辟许多内存,显得很浪费。由此出现了StringBuffer,StringBuffer带有16个初始容量的缓冲区大小,由线程安全的关键字synchronized修饰;而StringBuilder和StringBuffer大体类似,只是没有被synchronized修饰,线程不安全。
4.2 8种基本数据类型及其包装类
8种基本数据类型是boolean、char、byte、short、int、long、float和double,根据包装类的直接父类划分,boolean和char的直接父类是Object类,而byte、short、int、long、float和double的直接父类是Number。这里需要对包装类出现的合法性和合理性做一个说明,那就是8种基本数据类型不能因为引用类型的参数做运算,所以出现了自动装箱和自动拆箱的概念,即基本数据类型转为他们的包装类(本质上就是引用类)是为装箱,而由包装类转为基本数据类型是为拆箱。因此,包装类和基本数据类型都可以通过自动装箱或拆箱参与运算,给开发带来很大的便利性。
对于Object类,前文已有所讲述,读者可重新回顾一下Object的构造方法和普通方法。而对于Number类,这里做一个简单的说明。首先,Number是一个抽象类,因此不能直接实例化,下面列举2个Number类常用的几个方法:
①构造方法:Integer(int); Integer(String);
②xxxValue();将包装类转成对应的基本数据类型
Integer i = new Integer(12);
//以下:引用数据类型--->基本数据类型,拆箱
System.out.println(i.byteValue()); //12
System.out.println(i.shortValue()); //12
System.out.println(i.intValue()); //12
System.out.println(i.longValue()); //12
System.out.println(i.floatValue()); //12.0
System.out.println(i.doubleValue()); //12.0
由于Integer是基本数据类型对应的包装类的典型代表,所以下面以Integer举例说明包装类的使用:
Integer、Byte类的MAX_VALUE、MIN_VALUE的使用情况、Integer类的常用方法:
①static int parseInt(String s); //字符串转换成整型(—解析成int)
②static Integer valueOf(); //int转为Integer(—类名.谁的值)
③非静态intValue() 的使用情况如下:
package TestDemo3;
public class IntegerTest {
public static void main(String[] args) {
//Integer类的2个构造方法
Integer i1 = new Integer(123);
Integer i2 = new Integer("123");
System.out.println(i1); //123
System.out.println(i2); //123
//Integer常见的异常:数字格式化异常---NumberFormatException
//除此之外,开发中常见的异常还有:空指针异常、类型转换异常、数组下标越界异常
//Integer i3 = new Integer("中文");
//System.out.println(i3); //NumberFormatException
//Integer的常用静态方法:parseInt();字符串转换成整型 valueOf();基本数据类型转成对应的包装类
int i4 = Integer.parseInt("999");
Integer i5 = Integer.valueOf(222);
System.out.println(i4); //999
System.out.println(i5); //222
//用代码描述基本数据类型与String、包装类的关系(以Integer为例):
int i6 = 666;
//int--->String,2种方式
String s1 = String.valueOf(i6);
String s2 = i6 + "";
System.out.println(s1); //666
System.out.println(s2); //666
//String--->int
int i7 = Integer.parseInt("777");
System.out.println(i7); //777
//int--->Integer
Integer i8 = Integer.valueOf(888);
Integer i9 = new Integer("999");
//包装类对象的intValue();
int i10 = i9.intValue();
System.out.println(i8); //888
System.out.println(i10); //999
//Integer--->String
String s3 = String.valueOf("s3");
System.out.println(s3); //s3
//String--->Integer
Integer i11 = Integer.valueOf("11");
System.out.println(i11); //11
//总结,转成int的有2种方法:①引用类名.parseInt(); ②引用类对象.intValue();
//转成Interger:Interger.valueOf(参数);参数为int或String对象,分别对应2种情况
//转成String:String.value(参数); 参数为int或Integer对象,分别对应2种情况
//注意:一道经典的面试题:
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
Integer x = 127;
Integer y = 127;
System.out.println(x == y); //true
//解释原因如下:
//类加载时方法区会加载Integer类的整数型常量池,
// 不需要new,数值范围是-128~127
//可以用以下方法查询整数或者其他基本数据类型的最值
System.out.println(Integer.MAX_VALUE); //2147483647
System.out.println(Integer.MIN_VALUE); //-2147483648
System.out.println(Double.MAX_VALUE); //1.7976931348623157E308
System.out.println(Double.MIN_VALUE); //4.9E-324
}
}
4.3 日期类
传统日期时间
日期类是开发中关于计时的常用类,Date类就是日期类。Date类位于java.util包下,Date类已经重写了toString()方法,SimpleDateFormat类是与Date类相似的一个类,SimpleDateFormat类的作用是格式化日期,它的参数是年月日时分秒毫秒,具体格式的定义可以自行指定,但是字符必须按照“yyyy MM dd HH mm ss SSS”的字母写法来。详细用法见下列代码:
package TestDemo3;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTest {
public static void main(String[] args) throws ParseException {
//获取系统当前时间
Date date = new Date();
Date date1 = new Date(1);
System.out.println(date1); //Thu Jan 01 08:00:00 CST 1970
System.out.println(date); //Thu Oct 07 23:13:31 CST 2021
////以下:Date转String类
//以上输出并不符合中国人的阅读习惯,现在改进
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss-SSS");
String format = sdf.format(date);
System.out.println(format); //2021-10-07 23-17-19-892
//以下:String转Date类
String time = "2008-08-08 08-08-08-888";
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss-SSS");
Date date2 = sdf2.parse(time);
System.out.println(date2); //Fri Aug 08 08:08:08 CST 2008
//获取自1970-01-01 00:00:00: 000到当前系统时间的总毫秒数
long nowTimeMills = System.currentTimeMillis();
System.out.println(nowTimeMills); //1633620425904
//简单总结System类的相关属性和方法:
System.gc();
System.exit(0);
System.currentTimeMillis();
}
}
新日期时间
package com.simon;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTest01 {
public static void main(String[] args) {
//LocalDate、LocalTime、LocalDateTime分别是人读的日期、时间、日期和时间
LocalDateTime ldt1 = LocalDateTime.now();
//2021-10-29T22:36:33.825
System.out.println(ldt1);
LocalDateTime ldt3 = ldt1.minusDays(2);
//2021-10-27T22:38:04.572
System.out.println(ldt3);
LocalDateTime ldt2 = LocalDateTime.of(2021, 10, 29,22,36,49);
//2021-10-29T22:36:49
System.out.println(ldt2);
//格式化日期
DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime ldt = LocalDateTime.now();
String strDate = dtf.format(ldt);
//2021-10-29T22:41:48.982
System.out.println(strDate);
}
}
4.4 数字的格式化
开发中会涉及到数字的使用,数字的格式简单来说就是“#表示任意、,表示千分位,.表示小数点”。
详细情况见下列代码:
package TestDemo3;
import java.math.BigDecimal;
import java.text.DecimalFormat;
public class DecimalFormalTest {
public static void main(String[] args) {
//#表示任意数字
//,表示千分位
//.表示小数点
//0表示不够时补齐0
DecimalFormat df = new DecimalFormat("###,###.##");
String format = df.format(1234567.784);
System.out.println(format); //1,234,567.78
//BigDecimal是一个大数据类,精度极高,属于Java对象,专用在财务相关的软件中
BigDecimal bd1 = new BigDecimal(100);
BigDecimal bd2 = new BigDecimal(200);
//bd1+bd2
BigDecimal bdSum = bd1.add(bd2);
System.out.println(bdSum); //300
}
}
4.5 生成随机数
随机数是Java中较为常见的使用对象,见下列代码感受一下~
package TestDemo3;
import java.util.Random;
public class RandomTest {
public static void main(String[] args) {
Random rd1 = new Random();
boolean nb = rd1.nextBoolean();
//表示在参数范围内取值,左闭右开;若不指定,就在整数范围之内
Random rd2 = new Random();
int ni = rd2.nextInt(1024);
System.out.println(nb); //true
System.out.println(ni); //748
//小练习:随机生成5个不重复的0-100的随机数
}
}
4.6 枚举类
所谓枚举,就是能一枚一枚地列举出来。2种情况以内一般用Boolean型,而超过2种情况且刚好能列举出来一般用枚举比较合适,比如天气、颜色、四季、星期、月份。理解见下列代码:
package TestDemo3;
public class EunmTest {
public static void main(String[] args) {
//采用枚举的方式返回一个除法方法的执行成功或者失败的提示
Result retVal1 = divide(10, 0);
Result retVal2 = divide(10, 2);
System.out.println(retVal1); //FAIL
System.out.println(retVal2); //SUCCESS
}
public static Result divide(int a,int b){
try {
int c = a / b;
return Result.SUCCESS;
}catch (Exception e){
return Result.FAIL;
}
}
}
enum Result{
SUCCESS,FAIL
}
4.7 异常类
异常就是程序执行时遇到了问题。这些问题有的能被JVM解决,称为异常Exception;有的不能被解决,称为Error。异常出现的意义在于编译器检测到程序代码有问题时,会及时new一个异常的相关类并抛出,如果一直没有父类去接收并捕获异常的话,最终会抛给JVM,由于JVM无法处理,就会down机,并在此之前会打印输出语句,起一个警示的作用,程序员根据提示修改代码并重新运行。
Exception异常按照是否必须要对异常代码做处理,分为RuntimeExecption运行时异常、Exception编译时异常。运行时异常编程时可处理也可不处理,比如算术异常(1/0)、空指针异常、类型转换异常;编译时异常在编程时必须对其做处理,否则报错,所以编译时异常也被称为受检异常。
这里举个形象的例子。
编译时异常:出门前下大雨,不打伞可能会感冒,所以要带伞。带伞是异常处理之前的处理,也叫预处理,概率较高
运行时异常:小明走在大街上可能会被馅饼砸中,没必要对种异常进行处理.概率较低。
异常处理有2种方式:①继续向上抛出 ②try…catch…finally
继续向上抛出,一般是自身无法解决所以抛给调用者,但是不能无限向上抛出,最后如果JVM接手的话程序就会出错,所以能及早捕捉还是早点捕捉。捕捉就是try…catch…finally,关于catch有几点需要注意:
①catch后面的小括号的类型是其对应的准确的异常类型,也可以是该异常类型的父类型,这里体现了多态
②catch可以写多个,取决于被调用的方法throws了几个异常类型以及在方法里new了几个异常对
③catch写多个的时候,按照从上到下从小到大的原则
④jdk8 的一个新特性就是catch小括号内可以写多个“或”的异常类对象,如:catch(异常类1| 异常类2|异常类3 e).
关于try…catch…finally 中的finally有几点需要注意:
①finally里的语句块是一定要执行的,用于问题代码之后
②可以try…finally,finally也会执行,若try中有return,则return是最后一个执行的.总的来讲,除非try代码块中有
③System.exit(),否则finally一定会执行
异常类有2个常用的方法:
①exception.getMessage(); 简短描述信息
②exception.printStackTrace(); 异常追踪信息
printStackTrace会有jdk和我们自己代码的异常追踪记录,我们排查时,只需要关注我们写的那部分(高亮部分)的前几行代码记录,一般情况下,是我们前几行代码记录影响了后来调用者的代码记录,所以才会跟着报错。
下面有2道相关的面试题:
①以下程序输出结果是多少?
package demo1;
public class ExceptionTest2 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
//java语法规则:方法体中的代码必须自上而下执行,return最后执行
//反编译效果如下---金蝉脱壳:
// int i =100;
// int j = i;
// i++;
// return j;
public static int m(){
int i = 100;
try{
return i;
}finally {
i++;
}
}
}
②final finally finalize的区别有哪些?
- final是关键字,不可被继承和重写
- finally也是关键字,联合try使用,且一定会执行
- finalize()是Object类的一个方法,作为方法名出现,是一个标识符
日常开发中,我们使用地比较多的是自定义异常,一般是2个步骤,定义如下:
package demo1;
//自定义异常类
//2步:①继承 ②构造方法
public class MyException extends Exception {
public MyException(){
System.out.println("无参自定义异常");
}
public MyException(String s){
super(s);
}
}
package demo1;
public class ExceptionTest3 {
public static void main(String[] args) {
MyException me = new MyException("用户名不能为空");
//demo1.MyException: 用户名不能为空 at demo1.ExceptionTest3.main(ExceptionTest3.java:5)
me.printStackTrace();
String msg = me.getMessage();
System.out.println(msg); //用户名不能为空
}
}
注意:遇到要重写或者继承的时候,子类重写父类带抛出异常的方法时,子类抛出的方法只能更少,不能更多;抛运行时异常不受影响。即子类重写抛出的异常更好。
异常的体系结构
java.lang.Throwable:所有的错误和异常的父类
|—java.lang.Error:错误,一些严重的错误。比如:内存溢出、系统错误等等,无法处理;
|—java.lang.Exception:异常,我们应该尽可能预知并处理的异常,如:用户输入不匹配、网络连接 中断等。
|—编译时异常:编译时对其进行检查,若不处理,编译不能通过。
|—运行时异常:有时可以保证程序的正常运行,一旦发生异常,会在该代码处生成一 个异常对象,然后以堆栈式抛出,若不进行处理,程序终止运行。
错误
特点是代码无法处理。
package com.simon.exception;
public class ErrorTest01 {
public static void main(String[] args) {
//Exception in thread "main" java.lang.StackOverflowError
main(args);
//Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
byte[] bytes = new byte[Integer.MAX_VALUE];
}
}
编译时异常
红色下划线。
运行时异常
package com.simon.exception;
import java.util.Scanner;
public class ExceptionTest01 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入整数:");
int num = scanner.nextInt();
System.out.println(num);
}
}
空指针异常、类型转换异常、数组下标越界异常、算数异常等都属于运行时异常。
处理异常:抓抛模型
此处不再赘述。
处理异常:throws
throws是异常处理的方式之一,使用在方法的声明处,后面跟异常的类型。相当于推卸责任。
package com.simon.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest02 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("");
fis.read();
fis.close();
}
}
制造异常:throw
throw使用在方法体内,后面跟异常对象,不必一定使用Java自带的异常,这样更灵活。
package com.simon.exception;
public class ExceptionTest03 {
public static void main(String[] args) {
int result = div(10, 0);
System.out.println(result);
}
private static int div(int a,int b){
if (b==0){
throw new ArithmeticException("除数不能为0");
}
else {
return a/b;
}
}
}
自定义异常
见上。