宋禹宏 202302151260
第一章 初始Java
1.1.Java概述:
Oak(橡树,java的前身),詹姆斯·高斯林是“Java语言之父”。
在Java语言的发展历程中,Java被分成三个版本。
Java SE:定位在个人计算机上的应用。
Java EE:定位在服务器端的应用。
Java ME:定位在消费性电子产品的应用。
其中,随着塞班(Symbian)系统的淘汰,Java ME也随之沦为历史的尘埃。
Java是一门简单的、面向对象的优秀编程语言,它具有跨平台性、可移植性、安全性、健壮性、编译和解释性、高性能和动态性等特点,支持多线程、分布式计算与网络编程等高级特性。
1.2.面向对象的思想:
面向对象以对象为核心,强调事件的角色、主体。在宏观上使用面向对象进行把控,而微观上依然是面向过程。如果说面向过程的思想是执行者,那么面向对象的思想就是指挥者。
面向对象具有抽象、封装、继承、多态的特性,更符合程序设计中“高内聚、低耦合”的主旨,其编写的代码的可维护性、可读性、复用性、可扩展性远比面向过程高,但是性能相比面向过程要偏低一些。
第二章 Java编程基础
2.1.关键字和保留字
关键字:Java中预先定义好的一些有特别意义的单词,它们是构成Java语法的核心。
保留字:Java预先保留,在以后的版本中可能使用到的特殊标识符。
2.2.标识符与命名规范
标识符是给Java中的类、方法、变量、包命名的符号。
标识符需要遵守以下规则:
(1)标识符只能由字母、数字、下划线、美元符号组成,且不能以数字开头。
(2)Java标识符大小写敏感,长度无限制。
(3)标识符不能是Java的关键字和保留字。
2.3.数据类型
2.4.变量的定义与赋值
变量的本质是一个可进行操作的存储空间,其地址是确定的,但其内部所存储的内容不确定。
变量在使用前必须进行声明。
变量声明语法:数据变量 变量名;
数据类型 变量名1,变量名2,变量名3;
变量赋值语法:数据类型 变量名 = 变量值;
数据类型 变量名1 = 变量值1,数据类型 变量名2 = 变量值2,数据类型 变量名3 = 变量值3;
常量与变量的语法类似,只在变量声明语法前加上final关键字即可。变量是可以改变值的量,而常量一旦被赋值后,就不可以改变了。常量名的命名,一般要求所有字母大写,单词之间使用“ ”隔开。常量声明语法如下:
2.5.变量的类型转换
Java中的数据类型转换主要分为两种:自动类型转换(隐式转换)和强制类型转换(显式转换)。
在自动类型转换中int类型的常量可以直接赋值给char、short、byte,只要不超过它们能够表示的值的范围即可。
强制类型转换则可以强制性地将占用字节数多的数据类型的数据转换成占用字节数少的数据类型的数据,但这个转换过程可能会存在数据精度丢失的问题。强制类型转换的语法如下:
2.6.运算符与表达式
算数运算符
赋值运算符
关系运算符
逻辑运算符
位运算符
三元运算符
运算符的优先级
2.7.选择结构
if语句
if(条件表达式1){
//代码块1
}else if (条件表达式2){
//代码块2
}else if(条件表达式3){
//代码块3
}else {
//代码块n
}
switch语句
switch(变量){
case 值1:
代码块1;
break;
case 值2:
代码块2;
break;
...
default:
代码块n;
break;
}
选择结构的嵌套:
选择结构在使用上可以嵌套,if中的代码块也可以是switch语句,switch语句中的代码块也可以是if语句。通过嵌套,可以去判断更加复杂的逻辑。
两种选择结构对比:
if语句和switch语句都可以实现逻辑判断,但它们的使用场景有所不同。if语句一般用于区间值的判断,而switch语句只能用于确定值的判断。凡是switch语句能够实现的,if语句都可以实现,反之则不行。
2.8.循环结构
for 循环:适用于已知循环次数的情况,语法格式为:
while 循环:先判断条件,再执行循环体,语法格式为:
do-while 循环:先执行一次循环体,再判断条件,语法格式为:
break和continue语句
在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。而continue则只能终止某次循环,继续下一次循环。
循环语句的嵌套
如同选择结构一样,循环结构也可以任意嵌套。
2.9.方法
方法就是定义在类中的具有特定功能的一段独立小程序,用来完成某个功能操作。在某些语言中方法也称为函数或者过程。 在平时的开发中,不可能把程序的所有功能都写到main()方法中,这样维护起来成本很大,因此需要将功能拆分成一个一个方法,需要完成该功能时只需要调用对应方法即可。
语法格式:
方法的声明包含了很多组成部分,每个组成部分的含义如下:
(1)修饰符:用于控制方法的访问权限,目前学习阶段全部写为public static即可。
(2)返回值类型:方法需要返回给调用者数据的数据类型,如无返回值,必须声明返回值类型为void。
(3)方法名:方法的名字,命名规范在标识符规范的基础之上,采用首字母小写的驼峰命名规则。
(4)形参列表:由参数类型和参数名组成,也称作形式参数(形参),形参可以为任意多个,用于调用者给方法内部传递数据。
(5)方法体:该方法需要实现的具体逻辑。
(6)返回值:方法执行完毕后提供给调用者的数据。如果定义了返回值类型,那么返回值和返回值类型必须保持一致;如果定义的返回值类型为void,那么需要省略返回值,也就是直接用语句“return;”返回或者省略该语句直至该方法执行结束。当方法在执行过程中遇到return语句,就会返回而结束该方法的执行。
方法重载
在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可,这种现象称作方法重载。需要注意的是,方法重载只与参数和方法名有关,返回值类型不同,不构成方法的重载;形参的名称不同,不构成方法的重载;方法修饰符不同,不构成方法的重载。
方法递归
编程语言中,方法直接或间接调用方法本身,则称该方法为递归方法。合理的使用递归,能够解决很多使用循环难以解决的问题。
2.10.数组
数组就是一种能够存放相同数据类型的有序集合,或者说它是一个存储数据的容器。
语法格式:
1.通过索引操作元素 数组元素的操作都是通过索引(也称作下标)进行的,当创建了一个长度为n的数组后,它的索引范围是[0, n-1]。
!!!通过索引操作数组元素时,一定要注意索引不能超出数组下表范围,比如长度为3的数组中如果使用arr[3]来操作元素,程序运行时就会抛出“ArrayIndexOutOfBoundsException”,表示数组索引越界异常。
2.数组的遍历 当数组元素很多时,不可能一个一个使用索引获取元素,而是希望能够通过循环的方式取出数组中每一个元素,这个操作称作遍历。数组的遍历一般使用for循环遍历,从0遍历到数组长度-1即可。
3.数组的排序算法
1.冒泡排序 核心思想:在要排序的序列中,对当前还未排好序的全部元素,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的数往上冒,就好像水泡上浮一样,如果它们的顺序错误,就把它们交换过来,如下图所示。
2.选择排序 核心思想:在要排序的一组数中选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止,每一轮排序都是找出它的最小值或者最大值的过程,如下图所示。
二分查找法 又称折半查找法,其算法思想是每次查找数组最中间的值,通过比较大小关系,决定再从左边还是右边查询,直到查找到为止。二分查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入、删除操作困难。因此,折半查找法适用于不经常变动而查找频繁的有序列表,二分查找法依然需要使用到循环,但由于不知道循环次数,所以最好使用while循环实现。
二维数组
二维数组的每一个元素都是一个数组,其语法格式
Arrays工具类
Arrays是Java中提供的操作数组的工具类,通过Arrays类,可以很方便地操作数组。Arrays中提供了大量的方法,其中常见方法如下表所示。
2.11.JVM中的堆内存与栈内存
JVM是基于堆栈的虚拟机,堆栈是一种数据结构,是用来存储数据的。对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。 栈内存用于存储局部变量,以及对象的引用,它是一个连续的内存空间,由系统自动分配,性能较高。栈内存具有先进后出、后进先出的特点,虚拟机会为每条线程创建一个虚拟机栈,当执行方法时,虚拟机会创建出该方法的一个栈帧,该方法中所有的局部变量都会存储到这个栈帧中,方法执行完毕后,栈帧弹栈。 堆内存用于存储引用类型的数据,主要是对象和数组。全局只有一个堆内存,所有的线程共用一个堆内存。在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量访问堆中的数组或对象了。
学习总结:
在深入探索这一章节的学习过程中,我坚实地掌握了Java基础的核心概念与知识,这些构成了我未来在Java领域深入探索的坚固基石。特别是在学习选择结构和循环结构时,我深刻体会到其对于代码简化和程序可维护性的巨大价值。通过巧妙地运用选择和循环结构,我能够将复杂的逻辑条理清晰地展现出来,使得程序不仅易于理解,更便于后续的维护与扩展。
面对复杂的条件判断和循环逻辑,我学会了借助先进的数据结构和算法来优化程序,这一技能极大地提升了程序的执行效率。我认识到,在编程中,选择合适的工具和方法是至关重要的,它们能够让我们在面对挑战时更加游刃有余。
此外,方法的使用也为我带来了显著的收益。通过将代码模块化,我能够创建出更加简洁、可读性更强的程序,这不仅提高了代码的质量,更实现了代码的复用,使得我在开发过程中能够事半功倍。
总的来说,这一章节的学习不仅为我打下了坚实的Java基础,更让我学会了如何运用所学知识来编写高效、易于维护的代码。我深信,这些宝贵的经验和技能将在我未来的编程道路上发挥不可估量的作用。
第三章 面向对象程序设计
3.1.面向对象的概念
第一章1.2节已介绍
3.2.面向对象编程
3.2.1类的定义
Java使用class关键字来定义一个类。一个Java文件中可以有多个类,但最多只能有一个public修饰的类,并且这个类的类名必须与文件名完全一致。此外,类名需要符合标识符规则,并且遵循大写字母开头的驼峰规则。
3.2.2对象的创建与使用
要创建一个对象,必须先有一个类,然后通过new关键字来创建一个对象。
对象创建语法如下
!成员变量和成员方法隶属于对象,不同对象之间的成员变量占用不同的地址空间,互不影响。
成员变量默认值
为一个类定义成员变量时,可以显式地为其初始化。如果不为成员变量初始化,Java虚拟机也会默认给成员变量进行初始化。下表为Java虚拟机为每种数据类型的成员变量赋予的默认值。
匿名对象
通过使用new关键字来创建对象,这种对象分为两种:匿名对象与非匿名对象。何为匿名对象,何为非匿名对象呢? 匿名对象就是没有名字的对象,是定义对象的一种简写方式。 当方法的参数只需要一个对象,或者仅仅想调用一下某个对象的成员方法时,大可不必为这个对象单独创建一个变量,此时就可以使用匿名对象。
3.2.3构造方法
构造方法也称作构造器(constructor),用于给对象进行初始化操作,即为对象成员变量赋初始值。构造方法名称必须与类型相同,并且不能定义返回值,不能出现return关键字。构造方法的调用必须通过new关键字调用,语法格式如下所示。
构造方法的使用
Java中要求每一个类必须要有构造方法,当不在类中定义构造方法时,编译器会自动为类提供一个默认的无参数构造方法,一般简称为“无参构造方法”。前面通过new关键字创建Student对象时,就是在调用它默认的无参构造方法。
默认情况下,当系统没有为类提供任何一个构造方法时,只能使用系统默认的无参构造方法来创建对象;当只为类提供了无参构造方法时,就只能使用无参构造方法来创建对象;当为类同时提供了无参构造方法和有参构造方法时,不但可以使用无参构造方法来创建对象,还可以使用有参构造方法来创建对象。
new关键字的作用不仅仅是创建对象,它还会调用到对应的构造方法,并执行该构造方法中的方法体。
3.2.3this关键字
当创建一个对象成功后,Java虚拟机(JVM)会动态地分配一个引用,该引用指向的就是当前对象,该引用称作this。用更直白地说,this关键字就是在成员方法或者构造方法中使用,用来调用当前对象的成员变量、成员方法或构造方法,它代表当前对象。
this关键字可以调用成员变量、成员方法、构造方法。需要注意的是,成员方法中不能使用this关键字调用构造方法,当使用this关键字调用构造方法时,它必须出现在构造方法的第一行。
3.2.4static关键字
静态变量
在类中,将与成员变量同级的用static修饰的变量称为静态变量或类变量。静态变量优先于对象存在,随着类的加载就已经存在了,该类的所有实例共用这个静态变量,即共用同一份地址空间。当想调用静态变量时,可以使用对象名.变量名进行调用,但不推荐,建议使用类名.变量名进行调用。
静态方法
static关键字也可以修饰方法,用static修饰的方法称之为静态方法或类方法。静态方法同样是属于类的,优先于对象存在,调用方式与静态变量相同,也是建议使用类名.方法名进行调用。
与静态变量类似,静态方法虽然可以使用对象名调用,但依然不建议。此外,由于静态方法是属于类的,优先于对象存在,也就是说,当调用静态方法时可能程序并没有创建这个类的对象,因此在静态方法中不存在this引用,不能使用this关键字。
当一个类中几乎所有的核心方法都是静态方法时,称之为工具类。工具类中的方法都是工具性质的方法,它们的调用结果应当与对象无关,因此对于工具类,一般使用private关键字修饰它的空参构造方法,让其他类无法创建工具类的对象。
静态代码块
在类中,与成员变量和静态变量同级,使用大括号包裹起来的代码块称作构造代码块,当构造代码块使用static关键字修饰时,称作静态代码块。
构造代码块和静态代码块都只能定义在类中,与成员变量和静态变量平级,并且可以定义多个。不同的是,构造代码块随着对象的创建而加载,每创建一个对象就会执行一次;而静态代码块随着类的加载而加载,每个类中的静态代码块只会执行一次。
思考:当一个类中存在静态代码块、构造代码块、构造方法时,如果创建这个类的对象,它们三者的执行顺序应该是什么样的?
创建一个对象前,首先JVM会将这个类加载到方法区,当类被加载后,就会创建出它的静态变量,并执行静态代码块。之后,对象创建成功,初始化成员变量,并执行构造代码块。需要注意的是,对象其实并不是构造方法创建出来的,而是new关键字创建的。当创建了构造方法后,首先就会创建出对象,之后才会调用这个对象的构造方法。
结论:
(1)同一个类中,成员变量不能赋值给静态变量,静态变量可以赋值给成员变量和静态变量。 (2)同一个类中,静态方法不能调用成员变量和成员方法,成员方法可以调用静态或非静态的方法和变量。
(3)同一个类中,静态代码块不能调用成员变量和成员方法,构造代码块可以调用静态或非静态的方法和变量。
带有static关键字的方法、变量、代码块只能调用带有static关键字的方法、变量,而不带有static关键字的可以随意调用。
3.2.5包
包是Java中重要的类管理方式,开发中会遇到大量同名的类,通过包给类加上一个命名空间,很容易就能解决类重名的问题,也可以实现对类的有效管理。包对于类,相当于,文件夹对于文件的作用。
Java通过package关键字声明一个包,之后根据功能模块将对应的类放到对应的包中即可。包名的命名要求遵循标识符规则,在企业中一般以反着写企业域名的方式命名,比如com.baidu和com.jd,唯独需要注意的一点是,包名不能以java开头。
声明一个包的语法格式:
类的访问与导包
一般来说,定义的类都需要定义在包下。当要使用一个类时,这个类与当前程序在同一个包中,或者这个类是 java.lang 包中的类时通常可以省略掉包名,直接使用该类。其余情况下使用某一个类,必须导入包。
使用import关键字导入java中的包,语法格式如下:
3.3面向对象的三大特性
3.3.1封装
什么是封装
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。从Java面向对象程序设计的角度上分析,封装是指隐藏对象的属性和实现细节,仅对外提供公共的访问方式。
封装能够在一定程度上提高代码的安全性和复用性,使用时只需要了解使用方式,不需要知道内部细节。
访问修饰符
访问修饰符可以修饰类、接口、变量、方法,用于控制它们的访问权限。通过访问修饰符,可以灵活地控制哪些细节需要封装,哪些细节需要对外暴露。 Java中的访问修饰符:private、默认(无修饰符)、protected、public
get()/set()方法
将变量私有化,取而代之的是提供一些public的方法,用于给变量赋值或者获取变量,这些方法一般称之为“get()方法(getter)”和“set()方法(setter)”。其中,get()方法用于获取值,set()方法用于赋值。
get()方法和set()方法的命名是有要求的,比如get()方法,方法名必须以get开头,后面跟对应变量的变量名,并将变量名首字母大写。
当一个类中拥有age成员变量,但没有getAge()和setAge()方法,此时认为这个类中没有age属性,只有age变量。而如果拥有getAge()和setAge()方法,不管类中是否有age成员变量,该类都拥有age属性。这就是属性和成员变量的区别。
3.3.2继承
什么是继承
在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。 在Java中,继承使用extends关键字。从英文字面意思理解,extends的意思是“扩展”,即继承是在现有类的基础之上构建出的一个新的类,现有类被称作父类(也称作超类、基类等),新类称之为子类(派生类),子类拥有父类所有的属性和方法,并且还可以有自己独有的属性和方法。
语法格式如下所示
注意:Java中类之间只有单继承,因此一个子类只能有一个直接父类。但是Java支持多层继承,即A类继承B类,B类还可以继承C类,此时C类称为A类的间接父类。此外,如果一个类没有继承任何类,那么它会默认继承java.lang.Object。在Java中,Object类是所有类的父类,也就是说Java的所有类都继承了Object类,子类可以使用Object类的所有方法。
方法重写
当父类的方法不能满足子类的需求时,可以在子类中重写父类的方法,重写也称为复写或者覆盖。方法的重写需要注意以下四点:
(1)子类重写的方法必须与父类方法的方法名和参数列表完全一样。
(2)子类重写的方法修饰符权限要大于或等于父类方法的修饰符权限,如父类的修饰符是protected,那么子类重写该方法的修饰符必须是public或者protected,但父类方法如果是private或者static修饰,则不能重写。
(3)子类重写方法的返回值所属的类型必须小于或等于父类方法的返回值所属的类型。比如父类方法返回值是People类,则子类重写该方法返回值必须是People类或者其子类。
(4)父类方法返回值所属的类型如果是基本数据类型,子类重写该方法时,返回值类型必须与父类方法完全一致。
super关键字
super可以理解为直接父类对象的引用,或者说super指向子类对象的父类对象存储空间。可以通过super访问父类中被子类覆盖的方法或属性。
除private修饰的属性和方法外,子类可以通过super关键字调用到父类中的属性和方法,它的作用是解决子类和父类中属性、方法重名问题的。
super关键字也可以用来调用构造方法,使用方法是super(参数1, 参数2.....),如果构造方法中不存在super调用父类的构造方法,子类的构造方法第一行代码默认会被加上super(),来调用它的父类无参构造方法,因此如果父类不存在无参构造方法,程序编译就会出错,此时需要手动调用父类的有参构造方法。
super与this对比
创建对象并不是完全由构造方法决定的,因此即使使用super调用了父类的构造方法,但并没有创建父类对象。事实上,当类存在继承关系时,如果创建子类的对象,JVM会将该对象在堆内存中划分存储空间,父类中定义的属性和方法存储在父类的存储空间中,子类中定义的属性和方法存储在子类的存储空间中,二者都属于这一个对象。而super关键字指向的其实是该对象的父类存储空间,this关键字则指向整个对象
继承中的对象内存结构:
final关键字
final关键字可用来将一个变量声明成常量,事实上,final关键字还有其他场景的用法。final 是一个修饰符,它可以用来修饰类、类中的属性和方法以及局部变量,但是不能修饰构造方法。
final的用法主要有下面4点:
(1)final修饰的类不可以被继承,但是可以继承其他类。
(2)final修饰的方法不可以被重写。
(3)final修饰的变量是常量,只能被赋值一次。
(4)final修饰的引用类型变量不能改变它的引用地址,但是可以改变对象内部属性的值。
事实上,Java中有很多类都用关键字final修饰,比如最常见的String类。使用final修饰这些类的目的是不想让其他人扩展这些类,防止这些子类被使用到多态中,从而改变String类以及其他类本身具有的特性。 在软件开发中,也可以使用final关键字对一些类进行修饰,比如工具类,就可以私有化它的构造方法,让外界无法创建工具类的对象,并使用final关键字修饰类,让它无法被其他类继承,从而保证工具类的安全性。
Object类
java.lang.Object类是Java中所有类的父类(包括数组),如果一个类没有用extends关键字继承其他类,那么它默认就继承Object类。Java中这么设计的目的是让所有的类都拥有一些基本的方法,这些方法都定义在了Object类中,并且它们中的一部分可以被重写。
常见方法:
1. toString()方法
Object类中的toString ()方法返回该对象的字符串表示,默认情况下,执行Object类中toString()方法得到的结果是对象的类型@哈希码,比如User@6267c3bb。 这个结果很明显不太满足日常需要,因此在开发中一般会重写一个类的toString()方法,使其能够返回对象的所有属性值。
2. equals()方法 Object类中的equals()方法的作用是比较两个对象的地址是否相同,实际上在Java中,“==”运算符比较引用类型已经是比较地址是否相同了,equals()方法的功能与它相比略显多余,因此在开发中很少使用equals()方法本身的功能。 一般情况下,如果需要使用到equals()方法,会将其重写成比较内容是否相同。比如String类中的equals()方法就进行了重写,它的作用是比较两个字符串内容是否相同。
在People类中重写equals()方法,当两个People类的对象的name和age都相同时,就认为这两个对象是同一个人,如下所示。
3. hashCode()方法
Object类中的hashCode()方法用于计算一个对象的hash码值,hash码的作用是使用一串数字来示一个对象。一般来说,如果一个类中重写了equals()方法,就必须重写hashCode()方法,并且参与计算hash码值的属性必须与equals()方法中参与比较的属性一致。这么做是为了让对象在hash表中尽可能均匀地分散,比如后面将会学到的HashMap,底层比较对象是否相同时,就同时用到了equals()和hashCode()方法。
3.3.3多态
同一个方法调用,由于对象不同可能会有不同的行为。多态的前提是必须存在继承,并且子类重写了父类的方法,最重要的一点是父类引用要指向子类对象。
何为父类引用指向子类对象呢?比如上面的案例中,Teacher类和Worker类继承自People类,那么当创建Teacher类对象或者Worker类对象时,是可以使用People类型的变量接收的,如下所示。
好处:如果一个方法需要接收的参数类型是People,那么实际调用该方法时可以传入People类和它的任意子类对象,从而提高代码的复用性。此外,如果一个方法的返回值是People,那么也可以返回People类或它的任意子类对象。
多态的实现主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
引用类型数据转换
引用数据类型也存在着类型转换,与其说是类型转换,不如说是转换一个对象的引用。
引用类型数据转换分为以下两种。
向上转型:父类引用指向子类对象,属于自动类型转换。
格式:父类类型 变量名 = 子类对象;
向下转型:子类引用指向父类对象,属于强制类型转换。
格式:子类类型 变量名 = (子类类型) 父类对象;
向上转向隐藏了子类类型,提高了代码的扩展性,可以使一个方法的参数能够传入某个类的任意子类对象,但是多态会导致程序只能使用父类共性的内容,不能调用子类特有的方法。
向下转型则可以调用子类特有的方法,一般也可以称为强制类型转换。引用类型的向下转型是存在风险的,在转换之前必须保证待转换的引用是目标引用的对象或者子类对象,否则就会出现“ClassCastException”,即类型转换异常。使用向下转型时,一般会与instanceof关键字一起使用。instanceof关键字的作用是判断左边对象是否为右边类 (注意这里是类,并不是对象,可能有很多人说是对象)的实例(通俗易懂点来说就是:子类对象,或者右边类本身的对象),若是,则返回的boolean类型为true,否则,返回的boolean类型为false。
当一个类继承另一个类时,子类可以拥有独有的属性和方法,此时在多态中如果要调用它的属性和方法,那么调用的究竟是父类的还是子类的呢?
对于多态中的非静态方法,调用规则是:编译看左边,运行看右边,即在编译期间看左边的类中有无该方法,而实际在运行时执行的是右边类的方法。如果编译期间在左边的类没有找到该非静态方法,则会报编译错误。而对于多态中的静态方法,调用规则是:编译和运行都看左边。
继承中变量并不存在重写,也就是说,子类和父类如果都定义了name变量,那么这两个name是完全不同的变量,子类的name并不会覆盖父类的name,二者通过命名空间区分。因此,多态中调用一个变量的规则是:编译看左边,运行看左边,即编译和运行都看左边,也就是成员(静态)变量没有多态特性,无论右边是当前类还是当前类的子类,编译和运行期间执行的都是当前类中的方法。
Java多态中变量与方法调用时的一套口诀是:对于非静态方法,编译看左边,运行看右边;对于静态方法、静态变量、成员变量,编译和运行都看左边。
也就是说,子类的同名的静态方法、静态变量、成员变量,不会覆盖父类的,调用parent的这些属性找的是父类的属性;子类同名的非静态方法覆盖父类,调用parent的方法找的是子类的方法。调用parent的变量或方法会检查父类Parent是否存在此变量或方法,如果不存在(只有子类有)或者父类的变量或方法用private修饰,则编译通不过。
3.4抽象类
什么是抽象类
当编写一个类时,往往会为该类定义一些方法,这些方法用来描述该类功能的具体实现方式,这些方法都有具体的方法体。
但是有的时候,某个父类只是知道子类应该包含的方法,但是无法准确知道子类如何实现这些方法。比如一个“图形类”应该有一个“求周长”或/和“求面积”的方法,但“图形”这个类太抽象,不确定某个特定的图形,并不能知道它的周长和面积的计算公式,但不管怎样都可以确定的是,任何一个图形都会拥有计算周长和面积的方法。
此时,对于这种高度抽象的类,就可以定义为抽象类。而“计算周长”和/或“计算面积”的方法也过于抽象,以至于并不能知道一个“图形”的周长和面积是如何计算的,必须交给它的子类实现,此时就可以将这样的方法定义为抽象方法。
抽象的关键字是abstract,不管是抽象类还是抽象方法,都用abstract关键字修饰,语法格式如下所示。
注意: (1)抽象方法只有方法声明,没有方法体,它必须交给子类重写。子类重写抽象方法,也称作“实现”抽象方法。
(2)子类如果也是抽象类,则不一定需要实现父类的抽象方法,而如果不是抽象类,则必须要实现父类中所有的抽象方法。
(3)抽象方法必须被子类重写,因此抽象方法的访问修饰符不能是private。
(4)由于抽象方法没有具体的方法体,无法用一个抽象的对象调用它,因此抽象类不能被实例化。
(5)抽象类可以有构造方法,它的构造方法作用是方便子类创建对象时给抽象类的属性赋值。
接口interface是比“抽象类”还“抽象”的“抽象类”,可以更加规范地对子类进行约束。抽象类还提供某些具体实现,接口不提供任何具体实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。
接口的作用主要有两种:
(1)提供一种扩展性的功能,比如实现了Serializable接口的类就拥有了序列化的功能,实现了Clonable接口的类就拥有了克隆的功能。
(2)提供一种功能上的约束,或者说是一种规范。比如在面向接口开发的编程思想中,创建了OrderService接口,规定了接口中拥有订单的增加、删除、修改、查询方法,那么不管是什么订单,只要实现了OrderService接口,就必须拥有这四个方法。
定义接口不是使用关键字class,而是用interface修饰,语法格式如下所示。
在JDK 8以前,接口中的方法全部都是抽象方法,它们默认被public abstract修饰,因此在接口中定义方法就可以省略这两个关键字。在JDK 8及其之后,这个特性得到保留,并且还提供了static()方法和default()方法。
对于接口,可以通过子类去实现。实现接口的“子类”,往往称之为实现类。接口中的抽象方法,必须要“子类”去“继承”接口并“重写”这些方法,其语法格式为:
Java中类与类之间只有单继承,但是类与接口之间却允许有多实现。当一个类实现了多个接口时,只需要用逗号将这些接口隔开即可,如下所示:
3.5内部类
在描述事物时,若一个事物内部还包含其他事物,比如在描述汽车时,汽车中还包含发动机,这时发动机就可以使用内部类描述。一个定义在其他类内部的类称为内部类,而这个其他类则称为外部类。内部类分为成员内部类、静态内部类、局部内部类、匿名内部类四种。
成员内部类
成员内部类定义的位置与成员变量、成员方法同级。成员内部类定义语法格式如下所示。
内部类与外部类之间的成员变量可以互相访问。外部类访问内部类变量时非常简单,使用内部类的对象名.变量名即可获取内部类的变量,而内部类访问外部类变量时,则需要通过外部类名.this.变量名获取。
静态内部类
静态内部类的定义与成员内部类很相似,它与静态变量和静态方法平级,使用static关键字进行修饰,语法格式如下所示。
静态内部类的定义与成员内部类的定义区别并不大,但需要注意的是,静态内部类中无法访问外部类的非静态属性和方法。
局部内部类
有时候某个类可能只在某个代码块中才会使用到,此时大可不必在外面定义出一个类,直接使用局部内部类即可。局部内部类定义方式与前面两个内部类的定义方式相似,只不过它是定义到代码块中,与局部变量平级,如下所示。
匿名内部类
匿名内部类是局部内部类的一个引申。当某个类可能只在某个代码块中才会使用到,并且这个类是某个接口的实现类,或者某个类的子类时,不必特意写这个类,直接创建接口或者抽象类即可。在创建过程中“顺便”重写它们的方法,这样就是一个匿名内部类的对象,语法格式如下所示。
一般情况下,极少定义局部内部类,因为这样显得代码不够整洁,而匿名内部类的使用场景却非常多,当一个方法仅仅是需要传入一个对象,并且这个对象只在这个方法中才会用到时,就可以使用匿名内部类。
学习总结:
在学习面向对象编程(OOP)的深刻旅程中,我不仅深化了对多个核心概念的理解,还掌握了如何将这些概念融入实际编程实践中,从而显著提升了代码的可维护性、复用性和灵活性。这一学习过程不仅让我掌握了编程技能,更让我对高质量、结构化代码的重要性有了全新认识。
封装,作为OOP的基石,通过将数据和操作数据的方法紧密结合,确保了对象内部状态的安全性和完整性。利用private修饰符隐藏类的属性,并通过getter和setter方法进行访问和修改,有效避免了外部对对象内部状态的误操作,从而提升了程序的安全性和可维护性。同时,继承作为OOP的另一大支柱,允许子类复用父类的属性和方法,极大促进了代码复用。在继承关系中,子类不仅继承了父类的代码,还可以通过重写父类方法来实现特定的行为,而super关键字的使用则有效避免了代码重复和歧义。
进一步地,我对final关键字有了更深刻的理解。final可以修饰类、方法和变量,有效地限制了不必要的修改,使得代码更加严谨和稳定。同时,Object类作为Java中所有类的父类,其提供的toString()和equals()等基础方法,使得对象的比较和打印更加方便。通过理解并重写这些方法,我提高了自定义类的灵活性,特别是在集合框架中,自定义这些方法对于控制对象的比较和哈希值生成至关重要。
多态,这一OOP的重要特性,允许不同类的对象使用相同的接口调用不同的实现。多态通过方法重载和方法重写两种方式,使得程序在运行时能够根据对象的实际类型决定调用哪个方法,从而实现灵活的行为切换,增强了代码的可扩展性和解耦性。在实现多态的过程中,抽象类和接口扮演了关键角色。抽象类可以包含已实现的方法和未实现的抽象方法,供子类实现;而接口则只定义方法签名,任何实现该接口的类都必须提供方法的具体实现。通过合理选择使用抽象类或接口,我能够设计出更加灵活和可扩展的代码结构。
此外,内部类这一Java中的有用特性也给我留下了深刻印象。内部类可以嵌套在外部类中,访问外部类的成员,包括私有成员。内部类的使用有助于组织代码,减少外部类与内部逻辑之间的耦合,增强了代码的封装性和可读性。Java支持多种类型的内部类,如成员内部类、静态内部类、局部内部类和匿名内部类,每种类型都有其特定的应用场景和优势。
综上所述,通过深入学习OOP的核心概念——封装、继承、多态、抽象类、接口和内部类,我不仅掌握了如何更好地组织代码,还显著提升了代码的复用性、可扩展性和可维护性。这些概念不仅在理论上帮助我理解了OOP的核心思想,更在实践中让我能够设计出更加灵活、清晰和高效的程序结构。通过合理运用这些特性,我能够构建出更加复杂且功能强大的应用程序,从而提高了编程能力和解决问题的效率。这一学习过程不仅让我成为了更加优秀的程序员,更让我对编程艺术有了更深刻的理解和热爱。
第四章 异常
异常的概念
概念:异常是程序在运行期间发生的不正常的事件,它会打断指令的正常执行流程。(开发过程中的语法错误和逻辑错误不属于异常)
Java程序在执行过程中所发生的异常事件可以分成两类:
Error:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError和OOM。一般不编写针对性代码进行处理。
Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性代码进行处理。
例如:
✓空指针访问
✓试图读取不存在的文件
✓网络连接中断
Java中异常(Exception)与错误(Error)都继承自Throwable类。Java中定义了大量的异常类,这些类对应了各种各样可能出现的异常事件,这些异常类都直接或间接地继承了Exception。Exception分为运行时异常(RuntimeException)和编译时异常,Error和RuntimeException由于在编译时不会进行检查,因此又称为不检查异常(UncheckedException),而编译时异常会在编译时进行检测,又称为可检查异常(CheckedException)。
Java异常处理
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁,优雅,并易于维护。
Java提供的是异常处理抓抛模型。
Java程序执行过程中如果出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。
异常对象的生成
由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个异常类实例对象并抛出—自动抛出。
由开发人员手动抛出。
try-catch-finally的使用
throws的使用
throw手动输出异常
编写程序时,需要考虑程序可能出现的各种问题,比如编写一个方法,对于方法中的参数就需要进行一定程度的校验。如果参数校验不通过,需要告诉调用者问题原因所在,这时候就需要抛出异常。
Java中提供了一个throw关键字,该关键字用于抛出异常。Java中的异常本质上也是类,抛出异常时,实际上是抛出一个异常的对象,并提供异常文本给调用者,最后结束该方法的运行。 抛出异常语法格式如下:
方法重写中的异常
当一个类的方法声明了一个编译时异常后,它的子类如果重写了该方法,重写方法声明的异常不能超过父类的异常。(运行时异常不受约束)
1. 父类方法没有声明异常,子类重写该方法不能声明异常。
2. 父类方法声明了异常,子类重写该方法可以不声明异常,或者只声明父类的异常或该异常的子类。
自定义异常类
目的:分门别类管理异常,当出现异常时能准确定位到异常发生的位置。
方法:自己写一个异常类,继承Exception,RuntimeException,并使用。
学习总结:深入探索Java的异常处理机制,让我深刻领悟了如何以优雅而高效的方式应对程序中的各类错误,从而确保程序的稳健运行与卓越的用户体验。通过精心运用try-catch结构、finally块以及自定义异常类,我能够精准捕获并妥善处理各种异常情况,显著提升代码的健壮性。在开发实践中,我愈发体会到,出色的异常管理不仅是提升程序可维护性的关键,更是让代码调试更加顺畅、扩展更为灵活的重要基石。这一系列的学习,不仅使我对异常处理的理解从简单的错误捕捉跃升至一种深邃的程序设计哲学,更极大地增强了我编写高质量、高可靠性代码的能力,为我在编程领域的精进奠定了坚实的基础。
第五章 Java常用类
5.1包装类
让基本数据类型也可以像对象一样进行操作,在设计类时为每个基本数据类型设计了一个对应的类,这样,与这8个基本数据类型对应的类统称为包装类(Wrapper Class) 。
这8个类中,除了Character和Integer之外,其余6个包装类其实都是对应基本数据类型的首字母大写,非常方便记忆。其中,除Character和Boolean之外,其余的类都继承自Number类,称为数值类。数值类中都重写了Number的6个抽象方法:byteValue()、shortValue()、intValue()、longValue()、floatValue()、doubleValue(),这意味着6个数值类之间可以互相转换。
包装类的主要作用如下:
(1)让基本数据类型可以像对象一样进行操作,提供了更多方便的方法。
(2)提供了null值,让基本数据类型可以表示“未填写”的状态。
基本类型数据处于需要对象的环境中时,会自动转换为包装类,这就称为自动装箱。而包装类在需要数值的环境中时,会自动转换成基本类型,这称为自动拆箱。 自动装箱可以把基本数据类型直接赋值给包装类,而自动拆箱可以把包装类直接赋值给基本数据类型。
1. BigInteger
在实际开发中,可能面临着很大的整数运算,而Java的基本数据类型中,最大的long类型也是有最大值的,即263-1,想计算更大的数字时,就需要使用Java中大数字的类。
BigInteger是Java中表示大整型的类,它可以用来计算远大于long类型的数值。注意,BigInteger参与算术运算时,并不是用传统的加、减、乘、除符号,而是调用它的方法。BigInteger类中的常见方法如下表所示。
1. BigInteger 在实际开发中,可能面临着很大的整数运算,而Java的基本数据类型中,最大的long类型也是有最大值的,即263-1,想计算更大的数字时,就需要使用Java中大数字的类。
2. BigDecimal BigDecimal在使用时与BigInteger类似,它是用于表示大浮点数的类。BigDecimal常用于解决浮点数运算过程中的痛点,比如下面的代码在运行时结果可能会出乎意料。 System.out.println(0.1+0.2); 程序运行后,计算结果却不是0.3,因此在开发中不要使用double和float类型,以及它们对应的包装类型进行算术运算。为了解决这个痛点,Java提供了BigDecimal类。 创建BigDecimal对象时,必须给构造方法传入一个字符串作为它的值,如果传入了一个浮点数,依然会存在精度不准确的问题。此外,在使用BigDecimal进行除法运算时,必须指定保留小数位,否则当结果除不尽时,程序会抛出异常。
BigDecimal保留小数位使用的是setScale()方法,指定保留小数位个数以及保留方式,保留方式都是以BigDecima中静态常量的方式定义的,如下表所示。
5.2String类
String 类对象代表不可变的Unicode字符序列,内部使用了一个用final修饰的字符数组存储数据,一旦String的值确定了,就不能再改变了,每次通过截取、拼接等操作字符串时,都产生一个新的字符串。
Java为了方便起见,在使用字符串时也可以像基本数据类型一样直接对其进行赋值,但依然需要了解String的构造方法,如下表所示。
String类查找方法
String类转换方法
String类中的其他方法
5.3StringBuffer类与StringBuilder类
StringBuffer类
String代表着不可变的字符序列,每次拼接、截取字符串操作,都是重新创建一个字符串对象,如果这类操作过多,就会在内存中留下大量的无用字符串,比较占用内存。 StringBuffer类是抽象类AbstractStringBuilder的子类,代表可变的Unicode字符序列,即对StringBuffer执行转换操作时,都不会创建新的StringBuffer对象,自始至终操作的都是同一个字符串。StringBuffer并没有像String一样提供了简单的赋值方式,必须创建它的构造方法才可以。StringBuffer类中的构造方法如下表所示。
StringBuffer类常见方法
StringBuilder类
StringBuffer类和StringBuilder类非常类似,都是继承自抽象类AbstractStringBuilder类,均代表可变的Unicode字符序列。StringBuilder类和StringBuffer类方法一模一样,这里不再演示。不过StringBuilder不是线程安全的,这是与StringBuffer的主要区别:
(1)StringBuffer做线程同步检查,因此线程安全,效率较低。
(2)StringBuilder不做线程同步检查,因此线程不安全,效率较高。 因此在开发中如果不涉及字符串的改变,建议使用String,如果涉及并发问题,建议使用StringBuffer,如果不涉及并发问题,建议使用StringBuilder。
5.4时间和日期相关类
时间戳
以1970 年1月1日 00:00:00为基准时间,定义为0刻度,时间向前为负值,向后为正值,每一个刻度为1毫秒,称为时间戳,如下图所示。
时间戳中867686400000代表的就是1997年7月1日0时0分0秒,香港回归祖国怀抱的时间。 因为时间戳可能会非常长,因此Java中使用long类型来记录时间戳,用long类型的变量来表示时间,从基准时间往前几亿年,往后几亿年都能表示。
获取当前时间的“时刻数值”的方式非常简单,可以使用:long now=System.currentTimeMillis();
Date类
SimpleDateFormat类
SimpleDateFormat类是Java中的时间格式化类,通过该类可以将时间转换成用户想要的格式。 SimpleDateFormat对象在创建时需要指定时间格式,时间的格式通过一些特定的字符表示,当格式化时间遇到这些格式字符时,就会转换成对应的时间。时间格式字符如下表所示。
Calendar类
Date类一般用于表示时间,如果想计算时间,虽然通过时间戳也可以实现,但比较麻烦。Calendar是Java中的日历类,提供了日期时间的计算方式,通过Calendar类,可以准确地对年、月、日、时、分、秒进行计算。Calendar类中的主要方法如下表所示
Calendar类
在Calendar中可以设置指定字段的时间,如年、月、日,但方法中的时间字段是整数,Calendar中定义了很多静态常量,用这些静态常量可定义时间字段,如下表所示。
5.5 其他常用类
Math类
Math类是Java中与数学相关的类,提供了大量数学计算相关的方法,如开平方、三角函数、随机数等。Math类中的常用方法如下表所示。
Random类
Random类比Math类的random()方法提供了更多的方式来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围。当创建一个Random类之后,就可以使用它的方法获取随机数了。Random类中的主要方法如下表所示。
UUID类
UUID是通用唯一识别码(Universally Unique Identifier)的缩写,其目的是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端做辨识信息的指定。如此一来,每个人都可以创建不与其他人冲突的UUID。
枚举类
枚举是Java 5之后新增的特性,它是一种新的类型,允许用常量表示特定的数据片段,而且全部都以类型安全的形式来表示。枚举使用enum关键字定义,如下所示。
枚举值的命名方式与常量相同,要求以大写字母命名,单词之间使用下划线分隔。如下代码就是定义了一个季节枚举。
枚举的目的是列举出某个字段所有允许的取值,这些取值可能存在中文,因此在不少企业中放开了对中文的限制,允许枚举值使用中文定义。下面的枚举在一些企业里也是符合开发规范的。
学习总结:通过对这些常用Java类的深入学习,我的编程技能得到了显著提升,并且对Java的面向对象设计理念有了更为深刻的理解。在日常的项目开发中,我愈发感受到合理选择并善用这些类的重要性。它们不仅能够大幅提升开发效率,有效减少代码冗余,还极大地增强了代码的可读性和可维护性。Java类库的设计不仅功能强大,而且充满了智慧,它教会了我如何巧妙地利用现有工具来化解复杂的编程难题,让编程变得更加高效和优雅。