写在前面:
Java是面向对象的语言,面向对象也是java最最最重要的内容,可以说无对象不java。而面向对象的知识点其实是十分之多且繁杂的。为了避免学习的时候摸不着东西南北,所以在学习之前我们应该先在脑海中理清一些学习主线。
本篇把Java面向对象的学习主要分成三条主线,每条主线包含的内容如下:
- Java类及类的成员:属性、方法、构造器;代码块、内部类
- 面向对象的三大特征:封装性、继承性、多态性、(抽象性)
- 其他关键字:this、super、static、final、abstract、interface、package、import等(上面两条主线的内容已经涉及了一些关键字的使用,这条主线主要是学习一些其他关键字的使用)
本文并非是按串行的方式把三条主线的内容介绍,而是并行的方式去介绍这些内容,因为他们彼此之间是有许多关系的(就是说在学习中这些主线中的内容会相互有所杂糅)。列出学习主线只是把面向对象的知识点在脑海中树立起一个框架,把知识点归类起来,而不会感觉这学一点那学一点最终整个人是懵的!如果在下面的学习中感觉有点懵,请和我一起回来再看看这个学习主线!你就知道自己在面向对象中已经掌握了什么知识了!
可能有人会问?你说说什么是面向对象编程的思想?其实思想是一个挺虚无缥缈的东西。我们重点在这里主要关注的是思想的体现,编程思想的体现就是落实到代码设计(你的代码是怎么写的,把想法落实到代码中这就是编程思想的体现),所以本文主要以代码去落实面向对象编程的知识(把代码整明白了就会渐悟理解面向对象的思想到底是做什么的),而非纸上谈兵只是空谈思想!所以在学习中如果跟我一样对概念的东西感觉无法消化,不要纠结~代码敲起来吧!
面向对象思想概述
高级语言有两个过渡的阶段,一个是面向过程,一个是面对对象,来让我们看下他们的区别:
面向过程(POP)与面向对象(OOP)
- 二者都是一种思想,面向对象是相对于面向过程而言的。
面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。
面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。 - 面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。
有些童鞋看了这段是不是想吐槽:这又画下划线的又把字体加粗就能让人懂了吗?让我举起我的栗子!(简单体会一下,不用太纠结!)
/*
"人把大象装进冰箱" 用面向过程的思想去设计和面向对象的思想去设计:
1.面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做。
1)把冰箱门打开
2)抬起大象,塞进冰箱
3)把冰箱门关闭
2.面向对象:强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。(就是把功能封装到主体中,可以把名词做成类,把动词做成方法放到类中进行刻画,这里使用主体{功能}的形式去表述):
人{
打开(冰箱){
冰箱.打开();
}
抬起(大象){
大象.进入(冰箱);
}
关闭(冰箱){
冰箱.闭合();
}
}
冰箱{
打开(){}
闭合(){}
}
大象{
进入(冰箱){}
}
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了
面向对象是把构成问题事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
面向过程像是人自己去做事情,把事情按步骤慢慢去完成
而面向过程像是上帝视角去指导人去做事情,什么人能解决这个问题就让这个人做,没人能做就造人去做。
*/
其实我觉得面向过程到面向对象的过度可以理解为一个企业发展的过程:一开始的时候公司就几个人,这几个人不会明显分工各自做什么事,有任务来了就一起去做,这样会高效一些,这就是面向过程的思想。
当企业发展壮大上千上百人的时候,如果还是面向过程的做事情而不给这群人划分一下部门和职责的话那就显得特别混乱了,此时把公司分为几个部门如市场部、开发组、财务部(分类)等等给每个部门设计自己的部门职责和制度(部门的属性和功能),每个部门各司其职相互帮助。如果某个任务需要加一些别的功能需求,只需要在特定的部门添加上这些功能就完事了。这样整体就看上去非常的清晰、干净有流程了,这就是面向对象的设计思想
用面向对象的思想完成一个项目(或功能的思路):
面向对象的三大特征:
- 封装 (Encapsulation)
- 继承 (Inheritance)
- 多态 (Polymorphism)
面向对象有两个要素:类和对象,它们是面向对象的核心概念,核心概念,核心概念!
下面我们正式的进入面向对象的学习。
类和对象
面向对象思想的体现之一就是类和对象的使用:
类和对象的使用其实就是面向对象思想落地的实现,它大体上分成三步
-
创建类,设计类的成员(重点)
-
创建类的对象
-
通过“对象.属性” 或 “对象.方法”调用对象的结构
类:对一类事物的描述,是抽象的、概念上的定义
对象:是实际存在的该类事物的每个个体,因而也称为实例(instance)
- 面向对象程序设计的重点是类的设计
- 设计类,就是设计类的成员。
通过下面的图应该很容易理解:
简单理解类和对象与类和对象的关系:
- 类:抽象的、概念上的内容
- 对象:实实在在的一个个体(在java程序中的体现就是new出来的东西,在内存中是真正的创建这个对象并占据了一定的内存空间)
- 对象是由类派生出来的(new出来的)
理解:万事万物皆对象!(面试装逼技能)
实际上完整的表述是万事万物皆是类的对象!
- 在Java语言范畴中,我们都将功能、结构等封装到类中,通过类的实例化,来调用具体的功能结构,如:
- Scanner,String等
- 文件:File
- 网络资源:URL
- 涉及到Java语言与前端Html、后端的数据库交互时,前后端的结构在Java层面交互时,都体现为类、对象。
类的结构
上面简单理解了类,那么java中创建类的语法格式是什么?
知道类是怎么回事后,接下来了解下类的成员:
实际上,设计类,其实就是设计类的成员。类的成员除了有属性和方法外还有构造器;代码块、内部类。这里我们主要先简单讲讲常用到的类的成员:属性和方法。后面的内容中需要用到其他成员变量的时候我们再慢慢引入!循序渐进的去学习!
生活中描述事务无非就是描述事物的属性和行为。如:人有身高,体重等属性,有说话,游泳等行为。
属性和方法在现实中有许多种叫法,我们在这里做个统一化:
- 属性 = 成员变量 = field = 域、字段
- 方法 = 成员方法 = 函数 = method
老规矩,我们尽量把东西放到代码去说:
/*
* 一、设计类,其实就是设计类的成员
*
* 属性 = 成员变量 = field = 域、字段
* 方法 = 成员方法 = 函数 = method
*/
public class PersonTest {
}
class Person{
//属性
String name;
int age =1 ;
boolean isMale;
//方法:具备的一些功能
public void eat() {
System.out.println("恰饭");
}
public void sleep() {
System.out.println("睡觉");
}
public void talk(String language) {
System.out.println("说话,使用的是" + language);
}
}
对象
类可以比作是一张设计的图纸,在上面我们设计了人类及它的成员变量即设计好了一个类,但是这个类只是个图纸写着这是个什么东西有什么功能而已,如果不去使用它,即按图纸创建制造东西那这个类就显得毫无意义。
那么怎么去使用java类呢?
使用java类其实就是创建一个个具体的对象去做事情。而我们把创建类的对象这个过程就叫做类的实例化或实例化类过程
创建类的对象:
还是用上面人类的例子,代码说话:
/*
* 一、设计类,其实就是设计类的成员
*
* 属性 = 成员变量 = field = 域、字段
* 方法 = 成员方法 = 函数 = method
*
* 创建类的对象 = 类的实例化 = 实例化类
*
* 二、类和对象的使用(面向对象思想落地的实现) //重要所以不断强调
* 1.创建类,设计类的成员
* 2.创建类的对象
* 3.通过“对象.属性” 或 “对象.方法”调用对象的结构
*/
//测试类
public class PersonTest {
public static void main(String[] args) {
//创建Person类的对象
Person p1 = new Person();
//调用对象的结构:属性、方法
//调用属性:“对象.属性”
p1.name = "deehuang";
p1.isMale = true;
System.out.println(p1.name);//deehuang
//调用方法:“对象.方法”
p1.eat();
p1.sleep();
p1.talk("Chinese");
}
}
class Person{
//属性
String name;
int age =1 ;
boolean isMale;
//方法:具备的一些功能
public void eat() {
System.out.println("恰饭");
}
public void sleep() {
System.out.println("睡觉");
}
public void talk(String language) {
System.out.println("说话,使用的是" + language);
}
}
一开始说到,本文重点学习的是面向对象思想编程的体现,落地到代码中去学习面向对象编程的思想。所以为了方便学习,下面的内容我就像上面代码示例一样都把知识点尽量放到代码块中去说明演示并简单阐述。在不断的学习代码中会有渐悟甚至顿悟的过程,读者在看到一些概念性的东西无须停下来死扣。我认为入门学习面向对象知识最好的学习方式理解代码!理解代码!理解代码!
类与多个对象的关系
/*
* 一、设计类,其实就是设计类的成员
*
* 属性 = 成员变量 = field = 域、字段
* 方法 = 成员方法 = 函数 = method
*
* 创建类的对象 = 类的实例化 = 实例化类
*
* 二、类和对象的使用(面向对象思想落地的实现)
* 1.创建类,设计类的成员
* 2.创建类的对象
* 3.通过“对象.属性” 或 “对象.方法”调用对象的结果
*
* 三、如果创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性。(非static)
* 意味着:如果我们修改一个对象的属性a,则不影响另一个对象属性a的值
*/
//测试类
public class PersonTest {
public static void main(String[] args) {
//创建Person类的对象
Person p1 = new Person();
//调用对象的结构:属性、方法
//调用属性:“对象.属性”
p1.name = "Tom";
p1.isMale = true;
System.out.println(p1.name);//deehuang
//调用方法:“对象.方法”
p1.eat();
p1.sleep();
p1.talk("Chinese");
//***********************
Person p2 = new Person();
System.out.println(p2.name);//null
System.out.println(p2.isMale);//false
//***********************
//把p1变量保存的对象地址值赋值给p3,导致p1和p3指向堆空间中的同一个对象实体
Person p3 = p1;
System.out.println(p3.name);//deehuang
}
}
class Person{
//属性
String name;
int age =1 ;
boolean isMale;
//方法:具备的一些功能
public void eat() {
System.out.println("恰饭");
}
public void sleep() {
System.out.println("睡觉");
}
public void talk(String language) {
System.out.println("说话,使用的是" + language);
}
}
对象的内存解析
如果上面关于多个对象和类的关系有点难理解挺正常,这涉及了它底层的内存解析。我们来瞅瞅在JVM中对象的内存是怎么分配的:
先让我们来看下java虚拟机结构的图示:
java程序执行的过程:编译完源程序以后,生成一个或多个字节码文件。我们是使用JVM中的类的加载器(或装载器)和解释器对生成的字节码文件进行解释运行。意味着,需要将字节码文件对应的类加载到内存中(一个字节码文件对应一个类),这就涉及到内存解析(类与类的成员在内存中怎么分配调度)。
而我们对内存解析主要关注的是内存区域中的运行时数据区的方法区、堆和虚拟机栈的东西!
虚拟机栈:即为平时提到的栈结构。我们将局部变量存储在栈结构中
堆:我们将new出来的结构(比如:数组、对象)加载到堆空间中
。补充:对象的属性(非static)加载到堆空间中
方法区:类的加载信息、常量池、静态域
首先要明确一个前提:我们上面写的代码一般都是定义在main方法里面的,而我们说:方法中的变量都是局部变量。这意味着它们都存储在需虚拟机栈中!
我们还是通过一组代码加图解去看看对象的内存解析:
我们定义了一个人类:
class Person{
//属性
String name;
int age =1 ;
boolean isMale;
//方法:具备的一些功能
public void eat() {
System.out.println("恰饭");
}
public void sleep() {
System.out.println("睡觉");
}
public void talk(String language) {
System.out.println("说话,使用的是" + language);
}
}
如此详细明了的图解,已经不用多其他的了!
拓展:对象数组和匿名对象的使用
对象数组
/*
*对象数组题目:
定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)。
创建20个学生对象,学号为1到20,年级和成绩都由随机数确定。
问题一:打印出3年级(state值为3)的学生信息。
问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息
提示:
1) 生成随机数:Math.random(),返回值类型double;
2) 四舍五入取整:Math.round(double d),返回值类型long。
*
*
*/
public class StudentTest {
public static void main(String[] args) {
//创建20个学生对象
// Student s1 = new Student();
// Student s2 = new Student();
// Student s3 = new Student();
// Student s4 = new Student();
// Student s5 = new Student();
// Student s6 = new Student();
//一个个new也太low了!我们采用对象数组的方式快速创建20个Student对象
//声明Student类型的数组
Student[] stus = new Student[20]; //String[] arr = new String[10];
for(int i = 0;i < stus.length;i++){
//给数组元素赋值
stus[i] = new Student();
//给Student对象的属性赋值
stus[i].number = (i + 1);
//年级:[1,6]
stus[i].state = (int)(Math.random() * (6 - 1 + 1) + 1);
//成绩:[0,100]
stus[i].score = (int)(Math.random() * (100 - 0 + 1));
}
//遍历学生数组
for(int i = 0;i <stus.length;i++){
// System.out.println(stus[i].number + "," + stus[i].state
// + "," + stus[i].score);
System.out.println(stus[i].info());
}
System.out.println("********************");
//问题一:打印出3年级(state值为3)的学生信息。
for(int i = 0;i <stus.length;i++){
if(stus[i].state == 3){
System.out.println(stus[i].info());
}
}
System.out.println("********************");
//问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息
for(int i = 0;i < stus.length - 1;i++){
for(int j = 0;j < stus.length - 1 - i;j++){
if(stus[j].score > stus[j + 1].score){
//如果需要换序,交换的是数组的元素:Student对象!!!
Student temp = stus[j];
stus[j] = stus[j + 1];
stus[j + 1] = temp;
}
}
}
//遍历学生数组
for(int i = 0;i <stus.length;i++){
System.out.println(stus[i].info());
}
}
}
class Student{
int number;//学号
int state;//年级
int score;//成绩
//显示学生信息的方法
public String info(){
return "学号:" + number + ",年级:" + state + ",成绩:" + score;
}
}
对象数组内存解析
对象数组内存解析需要注意:
- 引用类型的变量,只可能存储两类值:null 或 地址值(含变量类型)
匿名对象
/*
* 匿名对象的使用
* 1. 理解:我们创建的对象,没有显式的赋给一个变量名。即为匿名对象
* 2. 特征:匿名对象只能调用一次
* 3.
*/
public class InstanceTest {
public static void main(String[] args) {
Phone p = new Phone();//p就是对象的变量名,我们称这个对象是有名的
p.sendEmail();
p.playGame();
//匿名对象
new Phone().sendEmail();
new Phone().playGame();
new Phone().price = 199;
new Phone().showPrice();//0.0
//说明匿名对象只能调用一次,每次调用都是一个 新的对象,互不影响
//匿名对象的使用
PhoneMall mall = new PhoneMall();
mall.show(new Phone());
//此时实际上是把对象赋值给了形参,所以在方法里面可以多次调用
}
}
class PhoneMall{
public void show(Phone phone) {
//外面的匿名对象赋值给了形参phone,所以对象在这里就变有名了
//可以多次调用,如下所示拿它调了两次方法
phone.sendEmail();
phone.playGame();
}
}
class Phone{
double price;//价格
public void sendEmail() {
System.out.println("发邮箱");
}
public void playGame() {
System.out.println("玩游戏");
}
public void showPrice() {
System.out.println(price);
}
类的成员
到此我们就对类和对象的使用有了初步的了解了。再次总结上面的对类和对象的使用实际上就是三个步骤的事情:
类和对象的使用(面向对象思想落地的实现)
-
1.**创建类,设计类的成员**
-
2.创建类的对象
-
3.通过“对象.属性” 或 “对象.方法”调用对象的结构
这三步我认为也是贯穿整个java开发的核心步骤。后面的学习也是基于这三步不断去强化的
接下来要回归正题:其中类的设计是OOP的重点,而类的设计实际上就是设计类的成员,故类的成员这块知识点你说重不重要!所以下面将对类的成员内容进行详细介绍。(有一部分成员在后面的内容在引入,因为需要和其他知识点搭配起来理解)
类的成员之一:属性
先通过图大致的了解一下属性基本结构
还是通过结合代码理解知识,在前面对对象的内存解析中我们提到了局部变量的概念,在代码中也会解决一些童鞋的疑惑:属性与局部变量的区别是什么?
/*
* 类中属性的使用
*
* 属性:是类中定义的变量,描述类具有的特点
*
* 属性(成员变量) vs 局部变量
* 1.相同点:
* 1.1 定义变量的格式:数据类型 变量名 = 变量值
* 1.2 先声明,后使用
* 1.3 变量都有其对应的作用域(这三点是所有变量的共性)
*
*
* 2.不同点:
* 2.1 在类中声明的位置不同:
* 属性:直接定义在类的一堆{}内
* 局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
*
* 2.2.关于权限修饰符的不同(权限修饰符详细内容在类的封装性去讲)
* 常用的权限修饰符:private、public、缺省(变量声明前面没有修饰符)、protected
* 如: private Sting name; public int age;根据修饰符不同就赋予了变量不同的权限
*
* 属性:可以在声明属性时,指明权限,使用权限修饰符
* 目前,大家声明属性时,都使用缺省就可以了
* 局部变量:不可以使用权限修饰符
*
* 2.3 默认初始化值的情况
* 当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外
*量类型都是引用类型。
* 属性:类的属性,根据其类型,都有默认初始化值
* 整型(byte、short、int、long) --> 0
* 浮点型(float、double) --> 0.0
* 字符型(char) --> 0(或'\u0000')
* 布尔型(boolean) --> false
* 引用数据类型(类、数组、接口、String) --> null
*
* 局部变量:没有初始化值。
* 意味着我们在的调局部变量之前,一定要显式赋值
* 特别的:形参在调用时,我们再赋值即可
*
* 2.4 在内存中加载的位置:
* 属性:加载到堆空间(非static)
* 局部变量:加载到栈空间
*/
public class UserTest {
public static void main(String[] args) {
User u1 = new User();
//属性的默认初始化值
System.out.println(u1.name);//null
System.out.println(u1.age);//0
System.out.println(u1.isMale);//false
// 局部变量没有初始化值:特别的:形参在调用时,我们再赋值即可
u1.talk("中文");
}
}
class User{
//属性(或成员变量)
String name;
public int age; //可以添加权限修饰符(了解)
boolean isMale;
//局部变量
public void talk(String language) {//language:形参,也是局部变量
System.out.println("我们使用" + language + "进行交流");
}
public void eat() {
//局部变量没有初始化,报错
//String food;//The local variable food may not have been initialized
String food = "煎饼果子";//局部变量
System.out.println("南方人爱吃:" + food);
}
}
你细品,细细地品!
成员变量和成员变量在内存的位置:
类的成员之二:方法
方法的声明与使用
不多说,直奔主题:类中方法的声明和使用:
/*
* 类中方法的声明和使用
*
* 方法:描述类应该具有的功能
* 比如:Math类:sqrt()开方操作、random()[0,1)的随机数、...
* Scanner类:nextXxx()用于从键盘获取数据 ...
* Arrays类:sort() \ binarySearch() \ toString() \ equals()\ ...
*
* 1. 举例几类方法的声明:
* public void eat(){} //无形参,void表示声明的方法没有返回值
* public void sleep(int hour){} //有形参
* public String getName(){} //无形参,类型 + 方法名表示有返回值(return)的方法
* public String getNation(String nation){}//有形参
*
* 2. 方法的声明:
* 权限修饰符 返回值类型 方法名(形参列表){
* 方法体;
* }
* 注意:static、final、abstract 来修饰的方法,后面再讲
*
* 3. 说明:
* 3.1 关于权限修饰符:默认方法的权限修饰符都使用publicJava规定的4种权限修饰符:
*private、public、缺省、protected -->封装性来细说
*
* 3.2 返回值类型:有返回值 vs 没有返回值
* 3.2.1
* 如果方法有返回值,则必须在方法声明时,指定返回值的类型,同时,方法中return关键字
*来返回指定类型的变量或者常量:"return 数据"。
* 如果方法没有返回值,则方法声明时,使用void来表示。就不使用return。但是如果使用
*的话,只能"return",表示结束此方法的意思
*
*
* 3.2.2 思考:我们定义方法该不该有返回值?
* ① 看题目要求
* ② 凭经验
*
* 3.3 方法名:属于标识符,遵循标识符的规则和规范:”见名知意“
*
* 3.4 形参列表:方法可以声明0个,1个,或多个形参
* 3.4.1 格式:数据类型1 形参1, 数据类型2 形参2,...
*
* 3.4.2 思考:我们定义方法时,该不该定义形参?
* ① 题目要求
* ② 凭经验
*
* 3.5 方法体:方法功能的体现
*
*
* return关键字的使用:
* 1.使用范围
* 2.作用:①结束方法 ②针对有返回值类型的方法使用'return 数据'方式返回所要的数据
* 3.注意点:return关键字的后面不可以声明执行语句
*
* 5. 方法的使用中,可以调用当前类的属性或方法
* 特殊:方法A中又调用了方法A:递归方法
* 方法中不能定义别的方法!
*/
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
cust.eat();
cust.sleep(6);
}
}
//客户类
class Customer{
//属性
String name;
int age;
boolean isMale;
//方法
public void eat() {
System.out.println("客户吃饭");
return;//void方法中使用return的意义是结束方法
//return后不可以加表达式
//System.out.println("HEOLL");
}
public void sleep(int hour) {
System.out.println("休息了" + hour + " 个小时");
eat();//方法的使用中可以调用当前类的方法
//sleep(10);//递归法,没有终止条件会抛stackoverflowError
//因为不断调用方法不断给声明形参会在栈中不断创建变量空间最终导致栈溢出
}
public String getName() {
return name;//方法的使用中可以调用当前类的属性
}
public String getNation(String nation) {
String info = "我的国籍是" + nation;
return info;
}
public void info() {
//public void play() {} //方法中不能定义另一个方法
}
}
方法的重载
方法的重载是比较偏概念性的东西。
总结一下构成重载的条件: "两同一不同"
- 同一个类、相同方法名
-
参数列表不同:参数个数不同,参数类型不同
/*
* 方法的重载(overload) loading...
*
* 1.定义:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。
*
* "两同一不同":同一个类、相同方法名
* 参数列表不同:参数个数不同,参数类型不同
*
* 2. 举例:
* Arrays类中重载的sort() / binarySearch()
*
* 3.判断是否是重载:
* 跟方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系!
*
* 4. 在通过对象调用方法时,如何确定某一个指定的方法:
* 方法名 ---> 参数列表
*/
public class OverLoadTest {
public static void main(String[] args) {
OverLoadTest test = new OverLoadTest();
test.getSum(1,2);
}
//如下的4个方法构成了重载
public void getSum(int i,int j){
System.out.println("1");
}
public void getSum(double d1,double d2){
System.out.println("2");
}
public void getSum(String s ,int i){
System.out.println("3");
}
public void getSum(int i,String s){
System.out.println("4");
}
//如下的3个方法不能与上述4个方法构成重载:因为参数列表相同(只与个数和类型有关)
// public int getSum(int i,int j){//跟返回值类型无关
// return 0;
// }
// public void getSum(int m,int n){//参数名不能作为标准,只看类型和个数
//
// }
// private void getSum(int i,int j){//跟修饰符无关
//
// }
}
重载概念的引入就是为了告诉我们在通过对象调用方法时,如何确定某一个指定的方法:方法名+ 参数列表
可变个数的形参的方法
/*
* 可变个数形参的方法
*
* 1.jdk 5.0新增的内容
* 2.具体使用:
* 2.1 可变个数形参的格式:数据类型 ... 变量名
* 2.2 当调用可变个数形参的方法时,传入的参数个数可以是:0个,1个,2个...
* 2.3 可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载
* 2.4 可变个数形参的方法与本类中方法名相同,形参类型也相同的数组之间不构成重载。换句话说,二者不能共存。
* 2.5 可变个数形参在方法的形参中,必须声明在末尾
* 2.6 可变个数形参在方法的形参中,最多只能声明一个可变形参(由于2.5自然的就有了2.6)
*
*/
public class MethodArgsTest {
public static void main(String[] args) {
MethodArgsTest test = new MethodArgsTest();
test.show(12);
// test.show("hello");
// test.show("hello","world");
// test.show();
test.show(new String[]{"AA","BB","CC"});
}
public void show(int i){
}
public void show(String s){
System.out.println("show(String)");
}//只传一个参数的话会优先匹配此方法
public void show(String ... strs){
System.out.println("show(String ... strs)");
//想要获取传入的每个参数,要使用循环遍历的方式取到,实际上strs就是一个数组
for(int i = 0;i < strs.length;i++){
System.out.println(strs[i]);
}
}
//不能与上一个方法同时存在
// public void show(String[] strs){
//
// }//5.0以前是使用数组的来定义可变个数形参的方法,所以会跟上面的方法重载冲突
//The variable argument type String of the method show must be the last parameter
//意思就是可变个数参数在方法形参中必须声明在最后末尾
// public void show(String ...strs,int i){
//
// }
}
方法参数的值传递机制
理解上面的内容其实需要明白java关于变量赋值的特性:
- 如果变量是基本数据类型,此时赋值的是变量所保存的数据值
- 如果变量是引用,此时赋值的是变量所保存的地址值
而方法形参的值传递机制其实和变量赋值就是一样的!
我们针对从形参是基本数据类型来看一下值传递机制
/*
方法的形参的传递机制:值传递
1. 形参:方法定义时,声明的小括号内的参数
实参:方法调用时,实际传递给形参的数据
2. 值传递机制:
如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值
*/
public class ValueTransferTest {
public static void main(String[] args) {
int m = 10;
int n = 20;
System.out.println("m = " + m + " , n = " + n);
//交换两个变量的值的方法
// int temp = m;
// m = n;
// n = temp;
//把它封装到方法里去实现
ValueTransferTest test = new ValueTransferTest();
test.swap(m, n);//这里的m和n就是实参
System.out.println("m = " + m + " , n = " + n);
//发现输出的是没有交换的结果??这是怎么回事?
}
public void swap(int m,int n) {//这里的m和n是实参
//交换两个变量的值的方法
int temp = m;
m = n;
n = temp;
}
}
解释上面的问题就需要从内存解析去下手了:
swap方法执行完后,它里面的变量的作用域消失,那栈中属于swap的局部变量就相继出栈。从始至终main方法中的局部变量m和n都没有变动。所以就出现了没有交换成功的现象
针对实参是引用数据类型来看:
/*
方法的形参的传递机制:值传递
1. 形参:方法定义时,声明的小括号内的参数
实参:方法调用时,实际传递给形参的数据
2. 值传递机制:
如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值
如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值(含变量的数据类型)
思考:
Person p1 = new Person();
User u1 = p1;//编译不通过为什么?
变量赋值要满足两点中的一点:类型一致、满足自动类型提升(不考虑强转的情况)
而这里就是因为引用类型变量存储的不单是地址值,还包括了变量的数据类型。u1的数据类型已经声明了是User类型了,而p1存的是Person类型+地址值。类型不一致故编译不通过。
*/
public class ValueTransferTest {
public static void main(String[] args) {
Data data = new Data();
data.m = 10;
data.n = 20;
System.out.println("m = " + m + " , n = " + n);
//交换m和n的值,这种方式是可以成功的
// int temp = data.m;
// data.m = data.n;
// data.n = temp;
//如果我们想把交换值的代码封装到方法中
ValueTransferTest test = new ValueTransferTest();
test.swap(data);//data是引用据类型,这里传递的是实参
System.out.println("m = " + m + " , n = " + n);
//交换成功!
}
pubilc void swap(Data data){
//交换m和n的值的方法
int temp = data.m;
data.m = data.n;
data.n = temp;
}
}
class Data{
int m;
int n;
}
上面如果传递值是引用数据类型,则可以成功,这是为什么呢?还是需要通过内存解析去解释:
对于参数是引用数据类型的方法,此时实参赋给形参的是实参存储数据的地址值。所以它们操作的是同一个对象实体故能成功交互值
到此已经画了不少内存图了,可想而知它的重要性,它对我们分析程序的运行有极大的帮助,在画内存图的过程中只要时刻谨记两个点,就不会出错:
- 内存结构:
- 栈:局部变量
- 堆:new出来的结构(非static)、成员变量、数组
- 变量:成员变量 vs 局部变量(方法内、方法形参、构造器内、构造器形参、代码块内)
PS:String类型在上面代码事例中会有特殊情况,这是因为它在内存中是存储在常量池中的,而非堆,关于String类型和方法区的内容我们以后再讲到。这里只要记住值传递机制的两个结论而不要去记上面的代码:
- 如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值
- 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值(含变量的数据类型)
简单记就是变量里存的是什么就传的是什么!
递归方法
/*
* 递归方法的使用(了解)
* 1.递归方法:一个方法体内调用它自身。
* 2. 方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制(不用自己写for和While)。
* 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环。
*
*
*
*
*/
public class RecursionTest {
public static void main(String[] args) {
// 例1:计算1-100之间所有自然数的和
// 方式一:
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println(sum);
// 方式二:
RecursionTest test = new RecursionTest();
int sum1 = test.getSum(100);
System.out.println(sum1);
System.out.println("*****************");
int value = test.f(10);
System.out.println(value);
}
// 例1:计算1-n之间所有自然数的和
public int getSum(int n) {// 3
if (n == 1) {
return 1;
} else {
return n + getSum(n - 1);
}
}
// 例2:计算1-n之间所有自然数的乘积:n!
public int getSum1(int n) {
if (n == 1) {
return 1;
} else {
return n * getSum1(n - 1);
}
}
//例3:已知有一个数列:f(0) = 1,f(1) = 4,f(n+2)=2*f(n+1) + f(n),
//其中n是大于0的整数,求f(10)的值。
public int f(int n){
if(n == 0){
return 1;
}else if(n == 1){
return 4;
}else{
// return f(n + 2) - 2 * f(n + 1);
return 2*f(n - 1) + f(n - 2);
}
}
面向对象特性之一:封装性
为什么在这里引入封装性的学习?因为我们要引出权限修饰符去介绍类与类的结构!前面已经说到,知识点并非死板的按主线去介绍。里面的许多知识点是相辅相成相互依赖的,根本没办法个说个的,本文主提倡的是循序渐进的方式去总结知识点和博主我的学习心得~所以读者看到这里突然冒出的封装性!不要懵!不要懵!不要懵!
什么是封装与隐藏
为什么要封装?
还是落地到代码去理解知识点:
/*
* 面向对象的特征一:封装与隐藏
* 一、 问题的引入:
* 当我们创建一个类的对象以后,我们可以通过"对象.属性"的方式,对对象的属性进行赋值。这里,赋值操作要受到属性的数据类型和存储范围的制约。除此之外,没有其他制约条件。但是在实际问题中,我们往往要给属性赋值加入额外的限制条件。这个限制条件就不能在属性声明时体现,我们只能通过方法进行限制条件添加(如:setLegs)同时,我们要避免用户再使用"对象.属性"的方式对属性进行赋值,则需要将属性声明为私有的(private)-->此时,针对于属性就体现了"封装性"。(把它隐藏起来了)
*
* 二、封装性的体现:
* 我们将类的属性xxx私有化(private),同时提供公共的(pubilc)方法来获取(getXxx)和设置(setXxx)此属性的值
* PS:这只是封装性的体现,上述不能说等同于封装性,封装性包括了很多体现!!!比如说一个人是好人,在公车上给老人让座只是好人的一个体现而已...还有很多事情体现了这是个好像人(请细品)
* 拓展:封装性的体现:① 如上 ② 不对外暴露的私有的方法 ③ 单例模式 ...
*
* 三、封装性的体现,需要权限修饰来配合。
*/
public class AnimalTest {
public static void main(String[] args) {
Animal a = new Animal();
a.name = "大黄";
a.age = 6;
// a.legs = -4;//-4只腿???
//这明显不合理,所以应该将legs属性保护起来,防止乱用。
//我们要加限制腿数的条件,只能把属性封装成方法去设置
//并且为了避免外界通过”对象.属性“的方式直接赋值,还要把属性声明为私有的(private)
//设置了限定方法,就不允许外界直接去通过调属性a.legs操作
// a.legs = 4;//The field Animal.legs is not visible
a.setLegs(6);
a.show();
a.setLegs(6);
a.show();
//既然legs属性隐藏起来然后提供设置legs的方法,当然还需要提供获取legs的方法
int l = a.getLegs();
System.out.println(l);
}
}
class Animal{
String name;
int age;
private int legs;//腿的个数,设置成私有权限,让外界不能直接调
//对属性的设置
public void setLegs(int l) {
//限制腿的个数只能是正数并且是以对数存在
if(l >= 0 && l % 2 == 0) {
legs = l;
}else {
legs = 0;
}
}
//对属性的获取
pubilc int getLegs(){
return legs;
}
public void eat() {
System.out.println("动物吃东西");
}
public void show() {
System.out.println("name = " + name + ", age = " + age + ", legs = " + legs);
}
}
在代码示例中我们简单体会了下封装性其中一个体现,不难看出要将封装性体现出来需要权限修饰符的配合!所以下面让我们看看常用的四种权限修饰符的理解:
四种权限修饰符
在这里我们只介绍三种权限:public、缺省和private,因为protected涉及了面向对象的继承性,所以留到学习继承性中去介绍会有更深的体会!
这里我们新建一个包名为io.deehuang.github.java1
/*
封装性的体现,需要权限修饰来配合。
1.Java规定的4种权限:(从小到大排列):private、缺省(啥也不写)、protected、public
2.4种权限可以用来修饰“类及类的内部”结构:属性、方法、构造器、内部类
3.具体的,4种权限都可以用来修饰”类的内部“结构:属性、方法、构造器、内部类
修饰类的话,只能使用:缺省、public
*/
public class Order{
private int orderPrivate;//私有
int orderDefault;//缺省
public int orderPublic;//公有
//私有方法
private void methodPrivate(){
int orderPrivate = 1;
int orderDefault = 2;
int orderPublic = 3;
}
//缺省
void methodDefault(){
int orderPrivate = 1;
int orderDefault = 2;
int orderPublic = 3;
}
//公有
public void methodPublic(){
int orderPrivate = 1;
int orderDefault = 2;
int orderPublic = 3;
}
}
//这里再新建一个class文件写测试类,因为同一个文件下只能有一个public类
//The public type OderTest must be defined in its own file
public class OrderTest {
public static void main(String[] args) {
Order order = new Order();
order.orderDefault = 1;
order.orderPublic = 2;
//出了Order类之后,私有的结构就不可以调用了
// order.orderPrivate = 3;//The field Order.orderPrivate is not visible
order.methodDefault();
order.methodPublic();
//出了Order类之后,私有的结构就不可以调用了
// order.methodPrivate();//The method methodPrivate() from the type Order is not visible
}
}
这里我们再新建一个包名为io.deehuang.github.java2
进行试图调Java1包的类结构去测试
/*
封装性的体现,需要权限修饰来配合。
1.Java规定的4种权限:(从小到大排列):private、缺省(啥也不写)、protected、public
2.4种权限可以用来修饰“类及类的内部”结构:属性、方法、构造器、内部类
3.具体的,4种权限都可以用来修饰”类的内部“结构:属性、方法、构造器、内部类
修饰类的话,只能使用:缺省、public
*/
import io.deehuang.github.java1.Order;
public class OrderTest {//不同包下的类名可以一样
public static void main(String[] args) {
Order order = new Order();
order.orderPublic = 2;
// 出了Order类所属的包之后,私有的结构、缺省声明的结构就不可以调用了
// order.orderDefault = 1;
// order.orderPrivate = 3;//The field Order.orderPrivate is not visible
order.methodPublic();
// 出了Order类所属的包之后,私有的结构、缺省声明的结构就不可以调用了
// order.methodDefault();
// order.methodPrivate();//The method methodPrivate() from the type Order is not visible
}
}
可以形象点理解四个权限:
总结封装性:
-
Java提供了4种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用时的可见性的大小
-
封装性的体现:
了解了封装性后,我们又回到主题,继续介绍回类的成员!
类的成员之三:构造器(或构造方法)
构造器
任何一个类,都有构造器!
根据参数不同,构造器可以分为如下两类:
- 隐式无参构造器(系统默认提供)
- 显式定义一个或多个构造器(无参、有参)
构造器的使用与说明:
/*
* 类的结构之三:构造器(或构造方法、constructor)的使用
* construct:建设、建造。 construction:CCB(中国建设银行) constructor:建设者
*
* 一、构造器的作用:
* 1.创建对象
* 2.初始化对象的信息
*
* 二、说明:
* 1.如果没有显式的定义类的构造器的话,则系统默认提供一个空参的构造器
* 2.定义构造器的格式:权限修饰符 类名(形参列表){}
* 3.一个类中定义的多个构造器,彼此构成重载
* 4.一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
* 5.一个类中,至少会有一个构造器。
*/
package io.deehuang.github.java1;
public class PersonTest {
public static void main(String[] args) {
//创建类的对象:new + 构造器
//如果没有显式的定义类的构造器的话,则系统默认提供一个空参的构造器
Person p = new Person();//我们将Person()这个结构就叫构造器
p.eat();
//在造对象的同时,给对象的属性赋值
//初始化对象的属性
Person p1 = new Person("DeeHuang");
}
}
class Person{
//属性
String name;
int age;
//构造器
public Person() {
System.out.println("我是构造器");
}
//发现把空参构造器注释掉后,下面写了不是空参的构造器,创建对象的时候编译失败
//这是因为一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
//可以有多个构造器,但是形参列表要不同:构造器的重载
public Person(String n) {
//在造对象的同时,给对象的属性赋值
name = n;
}
//可以有多个构造器,但是形参列表要不同:构造器的重载
public Person(String n, int a) {
//在造对象的同时,给对象的属性赋值
name = n;
age = a;
}
//方法
public void eat() {
System.out.println("人吃饭");
}
public void study() {
System.out.println("人学习");
}
}
上面通过构造器可以给属性赋值,我们前面说的通过
对象.属性
的方式也可以赋值或封装属性方法来给属性赋值甚至属性一开始还有默认初始化值或者显式初始化值(直接声明+赋值属性)。不知道各位童鞋是否有疑问:TM这么多值什么这跟什么%¥@!&*……?它们赋值的顺序和过程是怎么样的?莫浮躁~下面就简单介绍属性的赋值过程
属性的赋值过程
/*
* 总结:属性赋值的先后顺序
*
*
* ① 默认初始化
* ② 显式初始化
* ③ 构造器中初始化
* ps:上面三个赋值都是对象创建之前的操作,所以就叫初始化
*
* ④ 通过"对象.方法" 或 "对象.属性"的方式,赋值
*
* 以上操作的先后顺序:① - ② - ③ - ④
*
*/
public class UserTest {
public static void main(String[] args) {
User u = new User();
System.out.println(u.age);//1 说明显式赋值在默认初始化后面
User u1 = new User(2);
System.out.println(u1.age);//2 说明构造器初始化在显式赋值的后面
u1.setAge(3);
u1.setAge(5);
System.out.println(u1.age);//3 说明“对象.方法”方式赋值在构造器初始化后面
}
}
class User{
String name;
int age = 1;
public User(){
}
public User(int a){
age = a;
}
public void setAge(int a){
age = a;
}
}
拓展:JavaBean和UML类图
了解一下JavaBean和UML图(随意看看就好,基础篇里用不着)
JavaBean
/*
* JavaBean是一种Java语言写成的可重用组件。
所谓JavaBean,是指符合如下标准的Java类:
>类是公共的
>有一个无参的公共的构造器
>有属性,且有对应的get、set方法
*
*/
public class Customer {
private int id;
private String name;
public Customer(){
}
public void setId(int i){
id = i;
}
public int getId(){
return id;
}
public void setName(String n){
name = n;
}
public String getName(){
return name;
}
}
UML类图
有什么用?经常会看到题目或者需求就是用UML类图表示的!
关键字:this的使用
跟封装性的知识点一样,引入在这里是为了能为后面的知识点学习铺垫!
还是落实到代码去理解:
/*
*
* this关键字的使用:
* 1.this可以用来修饰、调用:属性、方法、构造器
*
* 2.this修饰属性和方法:
* this理解为:当前对象 或 当前正在创建的对象
*
* 2.1 在类的方法中,我们可以使用"this.属性"或"this.方法"的方式,调用当前对象属性或方法。但是,
* 通常情况下,我们都选择省略"this."。特殊情况下,如果方法的形参和类的属性同名时,我们必须显式
* 的使用"this.变量"的方式,表明此变量是属性,而非形参。
*
* 2.2 在类的构造器中,我们可以使用"this.属性"或"this.方法"的方式,调用当前正在创建的对象属性或方
* 法。但是,通常情况下,我们都选择省略"this."。特殊情况下,如果构造器的形参和类的属性同名时,我们必
* 须显式的使用"this.变量"的方式,表明此变量是属性,而非形参。
*
* 3. this调用构造器
* ① 我们在类的构造器中,可以显式的使用"this(形参列表)"方式,调用本类中指定的其他构造器
* ② 构造器中不能通过"this(形参列表)"方式调用自己
* ③ 如果一个类中有n个构造器,则最多有 n - 1构造器中使用了"this(形参列表)"
* ④ 规定:"this(形参列表)"必须声明在当前构造器的首行
* ⑤ 构造器内部,最多只能声明一个"this(形参列表)",用来调用其他的构造器
*
*
*/
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.setAge(1);//0
System.out.println(p1.getAge());
}
}
class Person{
private String name;
private int age;
//通常情况下,我们都选择省略"this."。
//特殊情况下,如果方法的形参和类的属性同名时,我们必须显式的使用"this.变量"的方式,表明此变量是属性,而非形参。
public Person() {
this.eat();//this也可以调方法
//假如Person初始化时需要考虑40行代码(下面处省略四十行)
//那彼此构成重载的构造器都要重复写40行(代码冗余了)?low实在是low。
//此时可以使用this调用构造器(看下面的重载方法)
}
public Person(String name) {
this();//调用空参的构造器,防止代码冗余!
this.name = name;//构造器是正在创建的对象进行初始化,这里的this表示当前正在创建的对象
// this();//Constructor call must be the first statement in a constructor
//规定:"this(形参列表)"必须声明在当前构造器的首行
}
public void setName(String name) {
// name = name;//编译器会就近原则的找name 这里就是形参自己赋值给自己
this.name = name;//关键字this表示当前对象,就会去找对象的同名属性去了
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
public void eat(){
System.out.println("人吃饭");
this.study(); //this可以调用方法
}
public void study(){
System.out.println("人学习");
}
}
关键字:package、import的使用
package
包的使用与作用
为什么要引入包的概念呢?是为了更好的实现项目中类的管理,所以提供了包的概念
落实代码中创建一个包和class来看看
package io.deehuang.github.java1;//package声明类或接口所属的包,声明在源文件的首行
/*
*
* 一、package关键字的使用
* 1.为了更好的实现项目中类的管理,提供包的概念
* 2.使用package声明类或接口所属的包,声明在源文件的首行
* 3.包,属于标识符,遵循标识符的命名规则、规范(xxxyyyzzz):”见名知意“
* 4.每”.“一次,就代表一层文件目录
*
* 补充:同一个包下,不能命名同名的接口、类
* 不同的包下,可以命名同名的接口、类
*
*/
public class PackageImportTest {
}
以下是jdk中常用的一些包的介绍:
MVC设计模式
在包的作用中有介绍到MVC设计模式,这里就简单了解一下(这是javaWeb的知识点,感兴趣的先简单了解一下就可了!莫纠结)
import
直接落实代码中理解:
package io.deehuang.github.practice;//package声明类或接口所属的包,声明在源文件的首行
import java.util.*; //导入util下的所有结构
import io.deehuang.github.java1.PackageImportTest;//导入不同包的类
import io.deehuang.github.java.Person;//导入不同包的类
import java.lang.reflect.Field;//lang包的子包内结构需要显方导入
import static java.lang.System.*;
/*
*
* 一、package关键字的使用
* 1.为了更好的实现项目中类的管理,提供包的概念
* 2.使用package声明类或接口所属的包,声明在源文件的首行
* 3.包,属于标识符,遵循标识符的命名规则、规范(xxxyyyzzz):”见名知意“
* 4.每”.“一次,就代表一层文件目录
*
* 补充:同一个包下,不能命名同名的接口、类
* 不同的包下,可以命名同名的接口、类
*
*
* 二、import关键字的使用
* import:导入
* 1. 在源文件中显式的使用import结构导入指定包下的类、接口
* 2. 声明在包的声明和类的声明之间
* 3. 如果需要导入多个结构,则并列的写出即可
* 4. 可以使用"xxx.*"的方式,表示可以导入xxx包下所有的结构(不包括子包)
* 5. 如果使用的类或接口是java.lang包下定义的,可以省略import结构
* 6.如果使用的类或接口是本包下定义的,则可以省略import结构
* 7. 如果在源文件中,使用不同报下的同名的类,则必须至少有一个类需要以全类名的方式显示
* 8.使用"xxx.*"方式表明可以调用xxx包下的所有结构。但是如果使用的是xxx子包(包里创建的包)下的结构,则仍需要显式导入
* 9.import static:导入类或接口中的静态结构:属性或方法
*/
public class ImportTest {
public static void main(String[] args) {
String info = Arrays.toString(new int[] {1,2,3});//导入Arrays才能使用
ArrayList list = new ArrayList();//需要导入util包下的ArrayList
HashMap map = new HashMap();//也需要导入util包下的HashMap类
//像这种用到同一个包下多个结构的情况,不需要重复写import,可用" * "号导入
PackageImportTest pi = new PackageImportTest();//使用不同包的类需要导入
String str = "123";
System.out.println();
//像String和system属于Java的核心包,定义在lang下。不需要导入,可以直接使用
//如果在源文件中,使用不同报下的同名的类,则必须至少有一个类需要以全类名的方式显示
//假设有两个包下都有名为Person类,而我都想在这里用,着就会冲突!
Person p1 = new Person();//导入import io.deehuang.github.java1.Person;
//此时我还想导入java1包里的Person:使用全类名方式显示
io.deehuang.github.java2.Person p1 = new io.deehuang.github.java2.Person();
//使用"xxx.*"方式表明可以调用xxx包下的所有结构。但是如果使用的是xxx子包(包里创建的包)下的结构,则仍需要显式导入
Field field = null; //Field类是lang包下reflect包里的结构。因为是子包所以需要显式import,不可省略
System.out.println();
//import static:导入类或接口中的静态结构:属性或方法
out.println();//out静态结构在前面被导入后可以直接用
}
}
类的成员在本模块里只介绍了三个,本模块的内容至此也先告一段落了。其他成员如内部类和代码块因为需要引入继承等知识的概念才能更好的上手掌握,所有需要先进入到下一个模块的学习再在其中插入介绍。还是那句话学习是循序渐进的过程,知识点的杂糅是希望有承上启下的作用。所以后面的模块中突然介绍到别的类的成员的时候莫懵逼!建议各位童鞋可以多回去看看写在开头的话~
面向对象三大特性
面向对象的三大特性有:封装、继承、多态。
封装性的知识点在上面的类与类的结构模块中已经提到,所以这里主要介绍的是继承与多态,以及引入一些与之紧密联系的知识点(包括未介绍完的类的成员以及一些关键字的使用)
面向对象特性之二:继承性
继承性
继承性其实是一个非常好理解的知识点。首先看下面两个UML类图:
为描述和处理个人信息,定义Person类:

为描述和处理学生信息,定义Student类:

我们发现,Student类里面除了school
属性外,其他的属性和方法都在Person类中定义过了。如果还有其他的比如Teacher类、Waiter类等等类需要定义相同的属性和行为的时,那我们这些方法都要重复的写一遍。这不仅十分麻烦而且会让我们的代码显得臃肿,这时候我们可能会想到能否有一种方式,让两个类“发生关系”,使得Student类去复用一下Person类的代码?Yes!这种方式就是继承。使用继承的方式去设计代码,可以减少代码冗余、大大提高代码的复用性!
在这里我们把继承的Person叫做Student的父类,而自然而然的Student就是Person的子类了。
继承的作用
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了关系,为之后多态性的使用提供了前提
注意:不要仅为了获取其他类中某个功能而去继承
继承性的使用
终究要回归代码层面去看看继承性是怎么回事
/*
*
* 面向对象的特征之二:继承性 why?
*
* 一、继承性的好处:
* ① 减少了代码的冗余,提高了代码的复用性
* ② 便于功能的扩展
* ③ 为之后多态性的使用,提供了前提
*
*
* 二、继承性的格式: class A extends B{}
* A:子类、派生类、subclass
* B:父类、超类、基类、superclass
*
* 2.1 体现:一旦子类A继承父类B以后,子类A就获取了父类B中声明的所有属性、方法
* 特别的:父类声明为private的属性和方法,子类继承父类以后,仍然认为获取父类中私有的结构。
* 只是因为封装性的影响,使得子类不能直接调用父类的结构而已
*
* 2.2 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的拓展
* 子类和父类的关系,不同于子集和集合的关系
* extends是延展、拓展的意思
*
*
*/
public class ExtendsTest {
public static void main(String[] args) {
//父类声明为private的属性和方法,子类继承父类以后,仍然认为获取父类中私有的结构
//只是因为封装性的影响,使得子类不能直接调用父类的结构而已
Student s1 = new Student();
// s1.age = 1; //父类的私有属性,出了类不能调。这是因为封装性的影响
s1.eat();
s1.name = "deehuang";
s1.setAge(10);
//如果没有继承过来即s1对象中不会有age属性,那么调用getAge()方法也会找不到age而报错
//能通过getAge方法获取private属性age说明子类会把父类声明private的属性也继承过来
System.out.println(s1.getAge());//10 说明是继承过来了
}
}
//父类
class Person {
String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println("恰饭");
}
public void sleep() {
System.out.println("睡觉");
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student extends Person {
//继承父类的属性不用自己重复写了
// String name;
// int age;
//子类继承父类以后,还可以声明自己的属性或方法:实现功能的拓展
String major;//年级---声明自己的属性
public Student() {
}
public Student(String name, int age, String major) {
this.name = name;
// this.age = age;//父类的私有属性子类不能调了!
this.getAge();
this.major = major;
}
//继承父类的方法不用自己重复写了
// public void eat() {
// System.out.println("恰饭");
// }
//
// public void sleep() {
// System.out.println("睡觉");
// }
//子类可与声明自己的属性或方法,实现功能的拓展
public void study() {
System.out.println("学习");
}
}
Java关于继承性的规定:
- 一个类可以被多个子类继承
- Java中类的单继承性:一个类只能有一个父类
- 子父类是相对的概念:一个子类也有可能是别的类的父类
- 子类直接继承的父类,称为:直接父类。间接继承的父类成为:间接父类
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法
讲到直接和间接的继承关系,就不得不提到Java中默认被所有类所有类直接或间接继承的类!
拓展:初探Object类
我们随便声明一个Person类,里面没有任何结构
public class Person {
}
在测试类中new一个Person类的对象,通过"."的方式发现Eclipse给我列出了一些不知从何而来的方法?并且说明了它们来自Object类!
这说明如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object
类
上面我们说到子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法的结论:可以推导出:
所有的java类(除java.lang.Object
类之外)都直接或间接地继承于Java.lang.Object
类。
也就意味着,所有的java类具有java.lang.Obeject
类声明的功能
所以Object也叫做根父类,它是所有类的父类!
这里的只是简单的引入下Object类,具体Object类的使用我们后面的另外起模块讲解!
了解了继承性之后我们就可以在此基础上学习更多主线中的知识点了
方法的重写
方法的重写这块知识点其实并不难,老规矩,东西都写在代码块中
/*
* 方法的重写(override / overwrite)
*
* 1.重写:子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作
*
* 2.应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法。
*
* 3. 重写的规定:
* 方法的声明: 权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型{
* //方法体
* }
* 约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
*
* ① 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同
* ② 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符(可以大于)
* >特殊情况:子类不能重写父类中声明为private权限的方法
* ③ 返回值类型:
* >父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
* >父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
* >父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)
* ④ 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型(具体放到JavaSE高级篇去说)
* **********************************************************************
* 子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。 具体放在static关键字模块中再去介绍
*/
public class PersonTest {
public static void main(String[] args) {
Student s = new Student("IT");
s.walk(10);//输出的了Person类中的show引证了3.②的结论
s.study();
Person p = new Person();
//重写以后,当创建子类对象后,通过子类对象调用父类中的同名同参数的方法时,实际执行的是子类重写父类的方法
s.eat();
p.eat();
}
}
class Person {
String name;
int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println("恰饭");
}
public void walk(int distance) {
System.out.println("走路:走了" + distance + "公里");
show();//private无法重写,所以调用的是父类的show
eat();//eat方法被重写,所以调用的是子类的eat()
}
//子类不能重写父类中声明为private权限的方法
private void show() {
System.out.println("我是一个人");
}
//父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
public Object info(){
return null;
}
}
class Student extends Person {
String major;// 专业
public Student() {
}
public Student(String major) {
this.major = major;
}
public void study() {
System.out.println("学习:专业是" + major);
}
// 对父类中的eat方法进行了重写
public void eat() {
System.out.println("学生在学生饭堂恰鸡腿");
}
// 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
// 父类是Public,此时编译报错
// void eat() {
//
// }
//
// 子类不能重写父类中声明为private权限的方法(这里就没有构成重写了)
private void show() {
System.out.println("我是一个学生");
}
//父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
public String info(){
return null;
}
}
再看4种权限修饰符
4种权限修饰符是Java面向对象中三大特性中的封装性的实现前提,我们使用4种权限修饰符来对类及类的结构设置对于外界可见度的大小实现封装。
对在封装性的模块中我们介绍到了4种权限修饰符的三种:pulibc、缺省和private。还有一种权限修饰符protected没有介绍,因为它的内容需要涉及到子类父类即类与类的继承关系的概念。所以放到这里再次引入。希望能在这里对四种权限修饰进一步体会。
再次回归一下四种权限修饰符:
在代码中体会4种不同的权限修饰
在同一个包中:
package io.github.deehuang.java1;
/*
* 体会4种不同的权限修饰
* 用不同的修饰符修饰类中的属性和方法,变量的名见名知意
*
*/
public class OrderTest {
public static void main(String[] args) {
Order order = new Order();
order.orderDefault = 1;
order.orderProtected = 2;
order.orderPublic = 3;
order.methodDefault();
order.methodProtected();
order.methodPublic();
//同一个包中的其他类,不可以调用Order类中私有的属性、方法
// order.orderPrivate = 4;//编译失败
// order.methodPrivate();//编译失败
}
class Order {
private int orderPrivate;
int orderDefault;
protected int orderProtected;
public int orderPublic;
//在同一个类中,四种修饰符修饰的类的成员都可以随意调用
private void methodPrivate(){
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
void methodDefault(){
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
protected void methodProtected(){
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
public void methodPublic(){
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
}
在不同的包中,声明一个SubOder类继承上面不同包下的Order类:
package io.github.deehuang.java2;
import package io.github.deehuang.java1.Order;
public class SubOrder extends Order {
public void method(){
orderProtected = 1;
orderPublic = 2;
methodProtected();
methodPublic();
//在不同包的子类中,不能调用Order类中声明为private和缺省权限的属性、方法
// orderDefault = 3;//编译失败
// orderPrivate = 4;//编译失败
//
// methodDefault();//编译失败
// methodPrivate();//编译失败
}
}
在不同的包声明一个与Order没有关系的OderTest类再进行测试:
package io.github.deehuang.java2;
import io.github.deehuang.java1.Order;
public class OrderTest {
public static void main(String[] args) {
Order order = new Order();
order.orderPublic = 1;
order.methodPublic();
//不同包下的普通类(非子类)要使用Order类,不可以调用声明为private、缺省、protected权限的属性、方法
// order.orderPrivate = 2;//编译失败
// order.orderDefault = 3;//编译失败
// order.orderProtected = 4;//编译失败
//
// order.methodPrivate();//编译失败
// order.methodDefault();//编译失败
// order.methodProtected();//编译失败
}
//在别的地方使用调对象.方法的形式去调对象.属性or方法也是如此
public void show(Order order){
order.orderPublic = 1;
order.methodPublic();
//不同包下的普通类(非子类)要使用Order类,不可以调用声明为private、缺省、protected权限的属性、方法
// order.orderPrivate = 2;//编译失败
// order.orderDefault = 3;//编译失败
// order.orderProtected = 4;//编译失败
//
// order.methodPrivate();//编译失败
// order.methodDefault();//编译失败
// order.methodProtected();//编译失败
//
}
}
在开发中,一般不是用最小(private)就是用最大(public)的权限修饰符,所以也不用花太多时间在这上边去搞清楚各种花样的调取方式。
关键字:super
super的使用
this表示本类对象的引用,super代表父类的内存空间的标识
老规矩,在代码块中叙述知识点:
/*
* super关键字的使用
* 1.super理解为:父类的
* 2.super可以用来调用:属性、方法、构造器
*
* 3.super的使用:调用属性和方法
*
* 3.1 我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用
* 父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
* 3.2 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的
* 使用"super.属性"的方式,表明调用的是父类中声明的属性。
* 3.3 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。
*
*
* 4.super调用构造器
* 4.1 我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
* 4.2 "super(形参列表)"的使用,必须声明在子类构造器的首行!
* 4.3 我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二选一,不能同时出现
* 4.4 在构造器的首行,没有显式的声明"this(形参列表)"或"super(形参列表)",则默认调用的是父类中空参的构造器:super()
* 4.5 在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器
*/
public class SuperTest {
public static void main(String[] args) {
Student s = new Student();
s.show();
System.out.println();
s.study();
Student s1 = new Student("deehuang", 18, "IT");
s1.show();
System.out.println("************");
Student s2 = new Student();
}
}
class Person {
String name;
int age;
int id = 1001;//身份证号
public Person(){
//在构造器的首行,没有显式的声明"this(形参列表)"或"super(形参列表)",则默认调用的是父类中空参的构造器:super()
System.out.println("我无处不在!");
}
public Person(String name){
this.name = name;
}
public Person(String name,int age){
this(name);
this.age = age;
}
public void eat(){
System.out.println("人:吃饭");
}
public void walk(){
System.out.println("人:走路");
}
}
class Student extends Person{
String major;
int id = 1002;//学号 ps:属性不存在覆盖一说,故内存空间就有两个id变量了
//我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
public Student(){
//构造器里即使什么都没写默认调的就是super();
}
public Student(String major){
super();//调用父类的空参构造器,
this.major = major;
}
public Student(String name,int age,String major){
//"super(形参列表)"的使用,必须声明在子类构造器的首行!
// this.name = name;
// this.age = age;
super(name,age);
this.major = major;
}
@Override
public void eat() {
System.out.println("学生:多吃有营养的食物");
}
public void study(){
System.out.println("学生:学习知识");
this.eat();//在子类去找
super.eat();//在父类去找
walk();//默认是this,现在自己这里找,找不到再去父类找
}
public void show(){
System.out.println("name = " + name + ", age = " + age);
System.out.println("id = " + id);//子类对象默认会调自己的属性,这里结果等于this.id
System.out.println("id = " + this.id);
System.out.println("id = " + super.id);//还想调父类的同名属性的话就需要使用super了
}
}
思考一个问题:
为什么我们的子类能过用父类的属性和方法还有构造器?
这需要让我看去聊下我们子类对象实例化的过程是怎么样的了:
子类对象实例化过程
- 从结果上来看(继承性):
- 子类继承父类以后,就获取了父类中声明的属性或方法
- 创建子类的对象,在堆空间中,就会加载所有父类声明的属性
- 从过程上来看:
- 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器(必定有一个构造器会调用
super()
或者不写默认调的也是super()
),进而调用父类的构造器.......直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类的结构,子类对象才可以考虑调用。
看一组图:
- 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器(必定有一个构造器会调用
虽然!!!我们在内存中加载过所有父类的构造器,但是要明确一点的是,在内存中到底造了几个对象???
一个!一个!一个!只是造了我们new出来的一个对象!只有new构造器才能说造了一个对象。
明确:虽然创建子类对象时,调用了父类的构造器。但是自始至终就创建了一个对象,即new的子类对象。
思考:为什么super(...)或this(...)调用语句只能作为构造器的第一句出现?
无论通过哪个构造器创建子对象,需要保证先初始化父类。目的:当子类继承父类后,“继承”父类中所有的属性和方法,因此子类有必要知道父类如何为对象进行初始化(所以优先加载父类)
面向对象特性之三:多态性
多态性
/*
* 面向对象特征之三:多态性
*
* 1.理解多态性:可以理解为一个事物的多种形态。
* 2.何为多态性:
* 对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
*
* 3. 多态的使用:虚拟方法调用
* 有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。
* 总结:编译,看左边;运行,看右边。
* 注意: 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法
*
* 4.多态性的使用前提: ① 类的继承关系 ② 方法的重写
*
* 5.对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)
*
*
*/
public class PersonTest {
public static void main(String[] args) {
//之前我們是正常的創建一個對象并把它賦給相應類型的引用
Person p1 = new Person();
p1.eat();//正常的方法调用
Man man = new Man();
man.eat();//正常的方法调用
man.age = 25;
man.earnMoney();//正常的方法调用
//*************************************************
//理解多态性:可以理解为一个事物的多种形态。
System.out.println("*******************");
//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
// Person p3 = new Woman();
//多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法 ---虚拟方法调用
p2.eat();
p2.walk();
//编译时子类只能调用父类中声明过的方法,因为编译的时候看的是变量声明的类型(左边)中是否有你要的类的结构
//当正在执行的时候才会发现子类的重写方法(右边)从而调用
// p2.earnMoney();//编译失败
//属性是在编译时确定的,编译时p2为Person类型,没有earnMoney成员变量,因而编译错误。
System.out.println(p2.id);//1001?1002?
//答案是1001!说明对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)
}
}
class Man extends Person{
boolean isSmoking;
int id = 1002;
public void earnMoney(){
System.out.println("男人负责挣钱养家");
}
public void eat(){
System.out.println("男人多吃肉,长肌肉");
}
public void walk(){
System.out.println("男人霸气的走路");
}
}
class Woman extends Person{
boolean isBeauty;
public void goShopping(){
System.out.println("女人喜欢购物");
}
public void eat(){
System.out.println("女人少吃,为了减肥");
}
public void walk(){
System.out.println("女人窈窕的走路");
}
}
为什么要有多态呢?多态的使用最明显的好处就是不用频繁的去写重载方法了!这一点很好理解,可以在后面开发中慢慢体会。
虚拟方法调用
这里还要再多讲讲虚拟方法调用,因为它是多态的重点!
上面提到动态绑定的概念:我们来从编译的角度看看重载和重写的不同来认识动态绑定和静态绑定:
重载不表现为多态性(编译和运行调用的都是父类的)
重写表现为多态性(编译认为调用是父类,执行的是子类对象---虚拟方法调用)
向下转型的使用
有了对象的多态性以后,内存中实际是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
既然内存中是有子类的属性和方法的那就说明它肯定是可以调用的,只是在编译的时候认为是没有罢了。那么我们怎么才能调用子类特有的属性和方法?这就需要使用向下转型了---使用强制类型转换符!
它和我们基本数据类型的类型转换有点相似,来看看它们的区别:
强转类型是可能失败的,转换只能是多态的情况下把父类变量强行转换成子类变量让我们可以调用子类对象的方法。试想如果你把一个男人类强行转换成女人类...这明显就不合理了!所以如果我们硬是转非多态情况下的引用类型,运行就会报错!(编译是可以通过的)。
为了避免这种运行错误的发生,我们要只能防范于未然,在开发中我们使用instanceof操作符来验证一个一个对象是否是一个类的对象。它的返回值是boolean型
还是用上面的Person与Man和Woman类举例:
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
//对象的多态性:父类的引用指向子类的对象
Person p2 = new Man();
// Person p3 = new Woman();
//多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法 ---虚拟方法调用
p2.eat();
p2.walk();
System.out.println("****************************");
//不能调用子类所特有的方法、属性:编译时,p2是Person类型。
p2.name = "Tom";
// p2.earnMoney();
// p2.isSmoking = true;
//有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致
//编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。
//如何才能调用子类特有的属性和方法?
//向下转型:使用强制类型转换符。
Man m1 = (Man)p2;
m1.earnMoney();
m1.isSmoking = true;
//使用强转时,可能出现ClassCastException的异常。
// Woman w1 = (Woman)p2;
// w1.goShopping();
/*
* instanceof关键字的使用
*
* a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false。
*
*
* 使用情境:为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先
* 进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
*
* 如果 a instanceof A返回true,则 a instanceof B也返回true.
* 其中,类B是类A的父类。
*/
if(p2 instanceof Woman){
Woman w1 = (Woman)p2;
w1.goShopping();
System.out.println("******Woman******");
}
if(p2 instanceof Man){
Man m2 = (Man)p2;
m2.earnMoney();
System.out.println("******Man******");
}
//如果 a instanceof A返回true,则 a instanceof B也返回true.
if(p2 instanceof Person){
System.out.println("******Person******");
}
if(p2 instanceof Object){
System.out.println("******Object******");
}
// if(p2 instanceof String){
//
// }
//练习:
//问题一:编译时通过,运行时不通过
//举例一:
// Person p3 = new Woman();
// Man m3 = (Man)p3;//编译认为P3是Person,使用向下转型符合要求。运行发现p3是Women,两个类没有继承关系,报类型转换错误。
//举例二:
// Person p4 = new Person();//非多态
// Man m4 = (Man)p4;//使用向下转型不通过,连向上转型的前提都没有是不可以通过向下转型转回去的
//问题二:编译通过,运行时也通过
// Object obj = new Woman();
// Person p = (Person)obj;
//问题三:编译不通过
// Man m5 = new Woman();
// String str = new Date();
// Object o = new Date();//能骗过编译器,但是运行时不能过
// String str1 = (String)o;
}
}
class Man extends Person{
boolean isSmoking;
int id = 1002;
public void earnMoney(){
System.out.println("男人负责挣钱养家");
}
public void eat(){
System.out.println("男人多吃肉,长肌肉");
}
public void walk(){
System.out.println("男人霸气的走路");
}
}
class Woman extends Person{
boolean isBeauty;
public void goShopping(){
System.out.println("女人喜欢购物");
}
public void eat(){
System.out.println("女人少吃,为了减肥");
}
public void walk(){
System.out.println("女人窈窕的走路");
}
}
So,怎么理解多态性?(有些知识还没有讲到,这里可以先粗略的了解一下)
至此面向对象的三大特性的知识点就介绍结束了。下面会对知识点做一些补充,例如继承中提到的所有类的父类Object类的使用,以及包装类(java的面向对象是针对类来讲的,基本数据类型用不了,为了完善面向对象,java对基本数据类型进行封装,就是我们引入的包装类)和其他关键字的使用。这两个常用类的使用知识点实际上与面向对象的主线无关,它们是我们开发中常用到的类,所以简单介绍一下
Object类的使用
Object类是所有类的根父类,所以其中的功能(属性和方法)就具有通用性。学习Object类的使用实际上就是学习它提供给我们的功能方法的使用,这里只介绍一些我们目前接触到的知识点的功能去说(因为有一些方法涉及到多进程多线程还有集合反射等知识):
-
Object类声明了一个空参的构造器
-
clone()
:复制一个对象,创建并返回一个对象的复制品(注意返回的是Object类型的对象) -
finalize()
:垃圾回收方法,当没有任何的引用指向一个对象的时候,垃圾收集器就会在在回收对象前调用当前对象的finalize方法。(通常我们不主动去调,这个方法是垃圾回收器自己会去调用的) -
getClass()
:获取当前对象的所属类 -
hashcode()
返回当前对象的哈希值 -
equals(Object obj)
:比较两个对象是否相等
== 和 equals的区别:/* * * 面试题: == 和 equals() 区别 * * 一、回顾 == 的使用: * == :运算符 * 1. 可以使用在基本数据类型变量和引用数据类型变量中 * 2. 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同) * 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体 * 补充: == 符号使用时,必须保证符号左右两边的变量类型一致。 * * 二、equals()方法的使用: * 1. 是一个方法,而非运算符 * 2. 只能适用于引用数据类型 * 3. Object类中equals()的定义: * public boolean equals(Object obj) { return (this == obj); } * 说明:Object类中定义的equals()和==的作用是相同的:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体 * * 4. 像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。 * * 5. 通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的"实体内容"是否相同。那么,我们就需要对Object类中的equals()进行重写. * 重写的原则:比较两个对象的实体内容是否相同. */ public class EqualsTest { public static void main(String[] args) { //基本数据类型 int i = 10; int j = 10; double d = 10.0; System.out.println(i == j);//true System.out.println(i == d);//true,做运算:自动类型提升 boolean b = true; // System.out.println(i == b);//基本数据类型之间运算不包括boolean型 char c = 10;//字符码值10与数字10是一样的 System.out.println(i == c);//true char c1 = 'A'; char c2 = 65; System.out.println(c1 == c2);//true,char参与运算自动类型提升为Int //引用类型 Customer cust1 = new Customer("Tom",21); Customer cust2 = new Customer("Tom",21); System.out.println(cust1 == cust2);//false //比较的是两个对象的地址是否相同,即两个引用(变量名)是否指向同一个对象的实体 String str1 = new String("deehuang"); String str2 = new String("deehuang"); System.out.println(str1 == str2);//false System.out.println("****************************"); System.out.println(cust1.equals(cust2));//false--->true //Object类中的equals()和==的作用是相同的,比较两个引用是否指向同一个实体 //像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。 System.out.println(str1.equals(str2));//true //String的euals方法进行过重写,所以不会按Object的方法进行声明了,String比较的是字符串是否相同 Date date1 = new Date(32432525324L); Date date2 = new Date(32432525324L); System.out.println(date1.equals(date2));//true //Date的euals方法进行过重写,所以不会按Object的方法进行声明了,Date比较的是时间戳(毫秒数)是否相同 } } class Customer { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Customer() { super(); } public Customer(String name, int age) { super(); this.name = name; this.age = age; } //自动生成的equals(),eclipse中的source中的generate equals() @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Customer other = (Customer) obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } //重写的原则:比较两个对象的实体内容(即:name和age)是否相同 //手动实现equals()的重写 // @Override // public boolean equals(Object obj) { // //// System.out.println("Customer equals()...."); // if (this == obj) { // return true; // } // // if(obj instanceof Customer){ // Customer cust = (Customer)obj; // //比较两个对象的每个属性是否都相同 //// if(this.age == cust.age && this.name.equals(cust.name)){ //// return true; //// }else{ //// return false; //// } // // //或 // return this.age == cust.age && this.name.equals(cust.name); // }else{ // return false; // // } // // }
总结:
-
toString()
:使用system.out.pirnt()
方法就是在调对象的toString()
方法,它的作用就是输出字符串值/* * Object类中toString()的使用: * * 1. 当我们输出一个对象的引用时,实际上就是调用当前对象的toString() * * 2. Object类中toString()的定义: * public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } * * 3. 像String、Date、File、包装类等都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回"实体内容"信息 * * 4. 自定义类也可以重写toString()方法,当调用此方法时,返回对象的"实体内容" */ public class ToStringTest { public static void main(String[] args) { Customer cust1 = new Customer("Tom",21); System.out.println(cust1.toString());//io.github.deehuang.java1.Customer@15db9742 System.out.println(cust1);//cio.github.deehuang.java1.Customer@15db9742 //像String、Date、File、包装类等都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回"实体内容"信息 String str = new String("MM"); System.out.println(str);//MM //String里面重写了toString所以输出的不是地址值 Date date = new Date(4534534534543L); System.out.println(date.toString());//Mon Sep 11 08:55:34 CST 2113 //Date里面重写了toString所以输出的不是地址值 } } class Customer { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Customer() { super(); } public Customer(String name, int age) { super(); this.name = name; this.age = age; } //手动实现 // @Override // public String toString() { // return "Customer[name = " + name + ",age = " + age + "]"; // } //自动实现eclipse中的source中的generate toString() @Override public String toString() { return "Customer [name=" + name + ", age=" + age + "]"; } }
包装类(wrapper)的使用
我们希望Java中的基本数据类型变量也具有面向对象的特征和功能,所以我们给每个基本数据类型进行封装,对基本数据类型封装的类就是我们的包装类
java提供了8种基本数据类型对应的包装类,使得基本数据类型的变量具有类的特征:
JDK5.0加入了自动装箱和拆箱的新特性,使得我们的基本数据类型能赋给包装类型的变量(自动装箱)或把包装类对象赋值给基本数据类型变量(自动拆箱)但前提是类型必须匹配!
基本数据类型、包装类、String三者之间的相互转换:
还是通过代码去学习理解:
import org.junit.Test;
/*
* 包装类的使用:
* 1.java提供了8种基本数据类型对应的包装类,使得基本数据类型的变量具有类的特征
*
* 2.掌握的:基本数据类型、包装类、String三者之间的相互转换
*
*
*
*/
public class WrapperTest {
//基本数据类型 --->包装类:调用包装类的构造器
@Test
public void test1(){
int num1 = 10;
// System.out.println(num1.toString());
Integer in1 = new Integer(num1);//调用包装类的构造器
System.out.println(in1.toString());
//包装类重写了ToString方法,输出的是实体内容,而不是地址值
Integer in2 = new Integer("123");//还可以放String
System.out.println(in2.toString());
//报异常,abc不是数
// Integer in3 = new Integer("123abc");
// System.out.println(in3.toString());
Float f1 = new Float(12.3f);
Float f2 = new Float("12.3");
System.out.println(f1);
System.out.println(f2);
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("TrUe");//忽略大小写
System.out.println(b2);//true
Boolean b3 = new Boolean("true123");
System.out.println(b3);//false
//只要不是true传入其他实参都会被当做是false
Order order = new Order();
System.out.println(order.isMale);//false
System.out.println(order.isFemale);//null,已经是个类了
}
//包装类--->基本数据类型:调用包装类Xxx的xxxValue()
@Test
public void test2(){
Integer in1 = new Integer(12);
int i1 = in1.intValue();
System.out.println(i1 + 1);
Float f1 = new Float(12.3);
float f2 = f1.floatValue();
System.out.println(f2 + 1);
}
/*
* JDK 5.0 新特性:自动装箱 与自动拆箱
*/
@Test
public void test3(){
// int num1 = 10;
// //基本数据类型-->包装类的对象
// method(num1);//基本数据类型没法丢到需要接收实参是引用类型的方法中
//自动装箱:基本数据类型 --->包装类
int num2 = 10;
Integer in1 = num2;//自动装箱
boolean b1 = true;
Boolean b2 = b1;//自动装箱
//自动拆箱:包装类--->基本数据类型
System.out.println(in1.toString());
int num3 = in1;//自动拆箱
}
public void method(Object obj){
System.out.println(obj);
}
//基本数据类型、包装类--->String类型:调用String重载的valueOf(Xxx xxx)
@Test
public void test4(){
int num1 = 10;
//方式1:连接运算
String str1 = num1 + "";
//方式2:调用String的valueOf(Xxx xxx)
float f1 = 12.3f;
String str2 = String.valueOf(f1);//"12.3"
Double d1 = new Double(12.4);
String str3 = String.valueOf(d1);
System.out.println(str2);
System.out.println(str3);//"12.4"
}
//String类型 --->基本数据类型、包装类:调用包装类的parseXxx(String s)
@Test
public void test5(){
String str1 = "123";
//错误的情况:
//两个没有子父类关系的类型进行强转编译报错
// int num1 = (int)str1;
// Integer in1 = (Integer)str1;
//可能会报NumberFormatException
//str1 = "123a"/ /a?? 报NumberFormatException,要确保数值是可以转换的!
int num2 = Integer.parseInt(str1);
System.out.println(num2 + 1);
String str2 = "true1";
boolean b1 = Boolean.parseBoolean(str2);
System.out.println(b1);
}
}
class Order{
boolean isMale;
Boolean isFemale;
}
来看三个包装类常见的面试题:
import org.junit.Test;
/*
* 关于包装类使用的面试题
*
*
*/
public class InterviewTest {
@Test
public void test1() {
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1);// 1.0
//因为三元运算符在编译的时候会要求两个结构的类型是同一类型,所以会对进行自动类型提升,故结果就变成了1.0
}
@Test
public void test2() {
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2);// 1
//这里就没有要求自动类型提升了所以是1
}
@Test
public void test3() {
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);//false
//Integer内部定义了IntegerCache结构,IntegerCache中定义了Integer[],
//保存了从-128~127范围的整数。如果我们使用自动装箱的方式,给Integer赋值的范围在
//-128~127范围内时,可以直接使用数组中的元素,不用再去new了。目的:提高效率
Integer m = 1;//引用指向的是缓存数组里拿Integer对象
Integer n = 1;//引用指向的是缓存数组Integer对象
System.out.println(m == n);//true,因为指向的是同一个对象
//超过了缓存数值的范围
Integer x = 128;//相当于new了一个Integer对象
Integer y = 128;//相当于new了一个Integer对象
System.out.println(x == y);//false
}
}
其他关键字及类的成员补充
有了前面的基础后就可以更好学习理解其他关键字和剩余两个类的成员了!
static关键字
到目前为止,当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new
关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。
我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下, 某些特定的数据在内存空间里只有一份。例如所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。
图中可以看到,因为中国是全部中国人公有的属性,这样的属性是没必要给每个实例对象分配单独的内存空间了(单独分配实在太浪费),这个时候我们就可以把这样的属性或者方法使用Static
关键字声明为类属性(静态属性)或者类方法(静态方法),这样这些属性和方法就实现类被所有对象所共享了!
类属性和类方法的设计思想:
static关键字的使用
下面还是通过代码来看看static关键字的使用:
/*
* static关键字的使用
*
* 1.static:静态的
* 2.static可以用来修饰:属性、方法、代码块、内部类
*
* 3.使用static修饰属性:静态变量(或类变量)
* 3.1 属性,按是否使用static修饰,又分为:静态属性 vs 非静态属性(实例变量)
* 实例变量:我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改。
* 静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
* 3.2 static修饰属性的其他说明:
* ① 静态变量随着类的加载而加载。可以通过"类.静态变量"的方式进行调用
* ② 静态变量的加载要早于对象的创建。
* ③ 由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。
* ④ 类和对象能否调用类变量和实例变量:
* 类变量 实例变量
* 类 yes no
* 对象 yes yes
*
* 3.3 静态属性举例:System.out; Math.PI;
*
* 4.使用static修饰方法:静态方法
* ① 随着类的加载而加载,可以通过"类.静态方法"的方式进行调用
* ② 静态方法 非静态方法
* 类 yes no
* 对象 yes yes
* ③ 静态方法中,只能调用静态的方法或属性
* 非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性
*
* 5. static注意点:
* 5.1 在静态的方法内,不能使用this关键字、super关键字
* 当你点击编译按钮时,也就是类加载时静态方法就被加载到了内存区,静态方法被优先执行,而此时对象都没被加载呢。this是当前类的对象,可想而知在静态方法执行的时候它还不存在呢,因此在静态方法中通过this调用其他任何东西都是扯淡。所以当然不能在静态方法区中使用this,super也是同理。
* 5.2 关于静态属性和静态方法的使用,大家都从生命周期(类和对象的生命周期)的角度去理解。
*
* 6. 开发中,如何确定一个属性是否要声明为static的?
* > 属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
* > 类中的常量也常常声明为static
*
* 开发中,如何确定一个方法是否要声明为static的?
* > 操作静态属性的方法,通常设置为static的
* > 工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections
*/
public class StaticTest {
public static void main(String[] args) {
//静态变量随着类的加载而加载。可以通过"类.静态变量"的方式进行调用
// 静态变量的加载要早于对象的创建。
//由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。
Chinese.nation = "中国";
Chinese c1 = new Chinese();
c1.name = "姚明";
c1.age = 40;
c1.nation = "CHN";
Chinese c2 = new Chinese();
c2.name = "马龙";
c2.age = 30;
c2.nation = "CHINA";
//静态属性举例:System.out
System.out.println(c1.nation);//CHINA,c2改了后c1的也会跟着改,这就是静态结构
//编译不通过,使用类去调实例属性,是调不了滴,试想一下每个对象都有自己的一套属性,这样调是调哪个对象的?
// Chinese.name = "张继科";
//实例调用非静态方法
c1.eat();
//使用static修饰方法:静态方法
// 随着类的加载而加载,可以通过"类.静态方法"的方式进行调用
Chinese.show();
//编译不通过,类无法调用非静态方法,试想一下每个对象都有自己的一套方法,这样调是调哪个对象的?
// Chinese.eat();
// Chinese.info();
}
}
//中国人
class Chinese{
String name;
int age;
static String nation;
public void eat(){
System.out.println("中国人吃中餐");
//非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性
//调用非静态结构
this.info();
System.out.println("name :" +name);
//调用静态结构
walk();
System.out.println("nation : " + nation);
}
//静态方法随着类的加载而加载
public static void show(){
System.out.println("我是一个中国人!");
//静态方法中,只能调用静态的方法或属性
//不能调用非静态的结构
// eat();
// name = "Tom";
//可以调用静态的结构
System.out.println(Chinese.nation);
walk();
}
public void info(){
System.out.println("name :" + name +",age : " + age);
}
public static void walk(){
}
}
注:
-
因为不需要实例就可以访问static方法,因此static方法内部不能有this。 (也不能有super ? YES!)
-
static修饰的方法不能被重写, 重写方法的目的是为了多态,或者说:重写是实现多态的前提,即重写是发生在继承中且是针对非static方法的。 语法上子类允许出现和父类只有方法体不一样其他都一模一样的static方法,但是在父类引用指向子类对象时,通过父类引用调用的依然是父类的static方法,而不是子类的static方法。
-
说到static应该马上想到随着类的加载而加载(所以static也不会去修饰类),主要是用来修饰类的内部结构,不能修饰构造器,因为构造器是处于类和对象之间的一道过程!它不会随着类的加载而加载,需要主动调用。
类变量和实例变量的内存解析
要了解类变量的内存解析,我们就要引入一个新的内存结构:方法区
栈主要存放的是局部变量;
堆放的是new出来的结构:对象和数组
方法区:类的加载信息、静态域、常量池。我们这里主要了解下静态域的结构
我们还是老规矩通过一组代码和图解去理解:
类在被使用时被加载到内存中!类的静态属性会被加载到方法区中的静态域中---记忆法:static静态 域field也称属性。
通过图可以了解到,静态的属性放的位置只有一份!所以无论通过类还是不同的对象去调取修改的话,获得的结果也是修改之后的,即全局共享。
理解main()方法的语法
我们知道
main()
方法是程序的入口,开始学习的时候因为要运行程序,所以我们会直接记住它的构造去使用,但是不清楚它的“内涵”,在学习完前面的知识后我们发现,对于main()
方法的语法,它的每个关键字和形参我们都已经学习过了。在这里就可以清晰的展开它的详细的语法构造
/*
main()方法的使用说明:
1.main()方法作为程序的入口
2.main()也是一个普通的静态方法
3.main()方法也可以作为我们与控制台交互的方式(之前:使用Scanner)
*/
public class MainTest {
public static void main(String[] args) {//入口
//main()方法也是一个普通的静态方法
Main.main(new String[100]);
//静态里面只能调静态,要调用非静态的属性和方法必须实例化对象,通过对象去调
MainTest test = new MainTest();
test.show();
}
public void show(){
}
}
class Main{
public static void main(String[] args) {
for(int i = 0;i < args.length;i++){
args[i] = "args_" + i;
System.out.println(args[i]);
}
}
}
main()方法也可以作为我们与控制台交互的方式(之前:使用Scanner)
通过main方法也可以总结出许多面向对象的知识:
类的成员之四:代码块
代码块的使用
代码块就是一对{}
还是通过代码去认识:
/*
* 类的成员之四:代码块(或初始化块)
*
* 1. 代码块的作用:用来初始化类、对象
* 2. 代码块如果有修饰的话,只能使用static.
* 3. 分类:静态代码块 vs 非静态代码块
*
* 4. 静态代码块
* >内部可以有输出语句
* >随着类的加载而执行,而且只执行一次
* >作用:初始化类的信息
* >如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
* >静态代码块的执行要优先于非静态代码块的执行
* >静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构
*
* 5. 非静态代码块
* >内部可以有输出语句
* >随着对象的创建而执行
* >每创建一个对象,就执行一次非静态代码块
* >作用:可以在创建对象时,对对象的属性等进行初始化
* >如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
* >非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法
*/
public class BlockTest {
public static void main(String[] args) {
String desc = Person.desc;//静态代码块随着类的加载而执行,
Person.info();//而且只执行一次
System.out.println(desc);
Person p1 = new Person();//非静态代码块随着对象的创建而执行
Person p2 = new Person();//每造一个对象都执行非静态代码块一次
System.out.println(p1.age);
}
}
class Person{
//属性
String name;
int age;
static String desc = "我是一个人";
//构造器
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
//代码块如果有修饰的话,只能使用static.---静态代码块 vs 非静态代码块
//非static的代码块
//如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
{
System.out.println("hello, block - 2");//可以有输出语句
}
{
System.out.println("hello, block - 1");
//调用非静态结构
age = 1;//作用:可以在创建对象时,对对象的属性等进行初始化
eat();
//调用静态结构,类的加载早于对象,所以调用静态的结构是完全可以的
desc = "我是一个爱学习的人1";
info();
}
//static的代码块
//如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
static{
System.out.println("hello,static block-2");//可以有输出语句
}
static{
System.out.println("hello,static block-1");
//调用静态结构
desc = "我是一个爱学习的人";//作用:初始化类的信息
info();
//不可以调用非静态结构,随着类的加载和执行,这个时候哪有非静态结构(它们随对象创建和加载)
// eat();
// name = "Tom";
}
//方法
public void eat(){
System.out.println("吃饭");
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public static void info(){
System.out.println("我是一个快乐的人!");
}
}
开发中,代码块的主要作用就是对类或对象的信息初始化,关于初始化我们前面也学习了对属性可以通过显式初始化赋值方式,也可以在构造器中进行初始化,代码块的功能就是给我们的初始化提供了多一个选择。我们完全可以不去使用它,因此在开发中代码块的使用频率是比较低的,这也是为什么要放到这里才去引入介绍的原因之一(还有是为了引入static关键字后去说明才能清晰的认识)。
属性赋值的先后顺序
代码块的执行要先于构造器!静态代码块会随着类的加载而加载,而创建一个对象,构造器"始终"都会先执行super()方法意味着会先去加载父类然后调用父类的构造器,所以父类的静态代码块最早执行然后是非静态代码块执行紧接着才是构造器。
赋值的规律遵从由父及子,静态先行!
注:静态代码块只随类的加载执行一次
注:代码块也称初始化块
要注意一点的是:main()
方法作为程序入口,实际上也是一个方法,只不过解释器在解释运行的时候自动的通过类去调用了!所以执行main()
方法前会先加载类!而加载类也会优先把该类的父类给加载了,这意味着会从根父类开始从执行“父类们”到该main方法所在类的静态代码块!!!
思考:代码块赋值和显式赋值谁先谁后呢?下面通过代码看一下也去做一个总结:
/*
* 对属性可以赋值的位置:
* ①默认初始化
* ②显式初始化/⑤在代码块中赋值
* ③构造器中初始化
* ④有了对象以后,可以通过"对象.属性"或"对象.方法"的方式,进行赋值
*
* 显示赋值和代码块根据摆放的顺序先后执行
* 执行的先后顺序:① - ② / ⑤ - ③ - ④
*/
public class OrderTest {
public static void main(String[] args) {
Order order = new Order();
System.out.println(order.orderId);//4
}
}
class Order{
//显示赋值和代码块根据摆放的顺序先后执行
int orderId = 3;
{
orderId = 4;
}
// {
// orderId = 4; //先执行代码块
// }
// int orderId = 3;//再显式赋值
}
关键字:final
/*
* final:最终的
*
* 1. final可以用来修饰的结构:类、方法、变量
*
* 2. final 用来修饰一个类:此类不能被其他类所继承。
* 比如:String类、System类、StringBuffer类
*
* 3. final 用来修饰方法:表明此方法不可以被重写
* 比如:Object类中getClass();
*
* 4. final 用来修饰变量:此时的"变量"就称为是一个常量
* 4.1 final修饰属性:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化
* 4.2 final修饰局部变量:
* 尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。
*
*/
public class FinalTest {
//final 用来修饰变量:此时的"变量"就称为是一个常量
//变量又分为类的属性和局部变量,让我们看看它们在final修饰下的区别:
// final int WIDTH; // final修饰属性不能使用默认初始化
final int WIDTH = 0;
final int LEFT;
final int RIGHT;
// final int DOWN;
{
LEFT = 1;// final修饰属性可以在代码块中初始化
}
public FinalTest(){
RIGHT = 2; // final修饰属性可以在构造器中初始化
}
public FinalTest(int n){
RIGHT = n;
}
// public void setDown(int down){
// this.DOWN = down; // 报错,final修饰属性在方法中给属性赋值是不行的
//这是因为构造器是对象出生的最后一道关卡,调用完构造器后在内存中对象就应该被加载了,final修饰的变量也会在堆空间出现,所以此时该变量应该是有值的,在方法中赋值显然太晚了。
//}
public void doWidth(){
// WIDTH = 20;//final修饰的变量不能修改值
}
//final修饰局部变量
public void show(){
final int NUM = 10;//常量
// NUM += 20;//报错,常量不能被修改
}
public void show(final int num){
//final修饰形参时,当我们调用此方法时,给该形参赋上实参,一旦赋值以后在方法内只能对该形参进行调用而不能进行修改
// num = 20;//编译不通过
System.out.println(num);
}
public static void main(String[] args) {
int num = 10;
num = num + 5;
FinalTest test = new FinalTest();
// test.setDown(3);
test.show(10);
}
}
//final用来修饰类---不能被继承
final class FinalA{
}
//报错,不能继承被final修饰的类
//class B extends FinalA{
//
//}
//报错,不能继承被final修饰的类
//class C extends String{
//
//}
class AA{
//final 用来修饰方法:表明此方法不可以被重写
public final void show(){
}
}
class BB extends AA{
//报错,final 用来修饰方法:表明此方法不可以被重写
// public void show(){
//
// }
}
在开发中我们还会经常遇到static final
组合搭配:
- static final 用来修饰属性:全局常量
- static体现的是随着类的加载而加载,final体现是不能变!
关键字:abstract---抽象类与方法
abstract
翻译为”抽象的“。它可以修饰类和方法。被修饰的类和方法就成为抽象类和抽象方法
抽象类:
简单粗暴的理解,使用abstract
修饰的类意味着这个类不能造对象了(不能实例化)!学习完多态后不难发现,我们在平时的使用中父类对象的使用就变得很少了, 一般都是将其引用指向各种丰富的子类对象,实现多态。父类不做事情只是把方法传承下去给子类去实现,这样的父类就可以定义为抽象类。
抽象方法:
提到抽象方法第一反应是:含有抽象方法的类一定是一个抽象类
还是放到代码中去理解
/*
* abstract关键字的使用
* 1.abstract:抽象的
* 2.abstract可以用来修饰的结构:类、方法
*
* 3. abstract修饰类:抽象类
* > 此类不能实例化
* > 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
* > 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作
*
*
* 4. abstract修饰方法:抽象方法
* > 抽象方法只有方法的声明,没有方法体
* > 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的。
* > 若子类重写了父类中的所有的抽象方法后,此子类方可实例化
* 若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰
*/
public class AbstractTest {
public static void main(String[] args) {
//一旦Person类抽象了,就不可实例化
// Person p1 = new Person();
// p1.eat();
}
}
abstract class Creature{
public abstract void breath();
}
//若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰
abstract class Person extends Creature{
String name;
int age;
// 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
//不是抽象方法:
// public void eat(){
//
// }
//抽象方法只有方法的声明,没有方法体
public abstract void eat();
//含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的(子类可以继承去用)。
public void walk(){
System.out.println("人走路");
}
}
class Student extends Person{
public Student(String name,int age){
super(name,age);
}
public Student(){
}
// 若子类重写了父类中的所有的抽象方法后,此子类方可实例化
//继承了抽象类的子类必须实现重写抽象类中定义的抽象方法(不然抽象方法什么都不做定义来有什么意义?就是为了让子类必须去实现重写)
public void eat(){
System.out.println("学生多吃有营养的食物");
}
//继承的抽象方法也必须去重写,子类必须实现重写抽象类中定义的所有抽象方法
@Override
public void breath() {
System.out.println("学生应该呼吸新鲜的没有雾霾的空气");
}
抽象类是用来模型化那些父类无法确定全部实现的方法,而是由其子类提供具体实现的对象的类
抽象类的应用:
absract使用中的注意点:
- abstract不能用来修饰:属性、构造器(不能重写)等结构
- abstract不能用来修饰私有方法(声明为private的方法不能被重写)、静态方法(不认为是重写覆盖)、final方法(直接就不让你重写)、final的类(不能继承)
其实并不要把抽象类想的很复杂,可以这样理解:
抽象类就是比普通类多定义了抽象方法(也可以没有),除了不能直接进行类的实例化操作之外,并没有任何的不同
补充:抽象类的匿名子类的使用
/*
* 抽象类的匿名子类
*
*/
public class PersonTest {
public static void main(String[] args) {
method(new Student());//匿名对象---没有变量名
Worker worker = new Worker();
method1(worker);//非匿名的类 非匿名的对象 ---有变量名 有类名
method1(new Worker());//非匿名的类 匿名的对象 --无变量名 有类名
System.out.println("********************");
//创建了一匿名子类的对象:p --- 有变量名 没类名
Person p = new Person(){
@Override
public void eat() {
System.out.println("吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸");
}
};
method1(p);
System.out.println("********************");
//创建匿名子类的匿名对象 ---无变量名 无类名
method1(new Person(){
@Override
public void eat() {
System.out.println("吃好吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸新鲜空气");
}
});
}
public static void method1(Person p){
p.eat();
p.breath();
}
public static void method(Student s){
}
}
abstract class Creature{
public abstract void breath();
}
//若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰
abstract class Person extends Creature{
String name;
int age;
// 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
public Person(){
}
public Person(String name,int age){
this.name = name;
this.age = age;
}
//不是抽象方法:
// public void eat(){
//
// }
//抽象方法只有方法的声明,没有方法体
public abstract void eat();
//含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的(子类可以继承去用)。
public void walk(){
System.out.println("人走路");
}
}
class Student extends Person{
public Student(String name,int age){
super(name,age);
}
public Student(){
}
// 若子类重写了父类中的所有的抽象方法后,此子类方可实例化
//继承了抽象类的子类必须实现重写抽象类中定义的抽象方法(不然抽象方法什么都不做定义来有什么意义?就是为了让子类必须去实现重写)
public void eat(){
System.out.println("学生多吃有营养的食物");
}
//继承的抽象方法也必须去重写,子类必须实现重写抽象类中定义的所有抽象方法
@Override
public void breath() {
System.out.println("学生应该呼吸新鲜的没有雾霾的空气");
}
}
class Worker extends Person{
@Override
public void eat() {
}
@Override
public void breath() {
}
}
匿名子类对象的意义:只用一次!懒得去设计类并且只需要用到一次的话就可以使用它。
关键字:interface---接口
什么是接口
接口的概述:
接口和类是两个并列的结构
接口的作用就是为了解决java不支持多继承的问题。一个类可以实现多个接口,一定程度上解决了单继承性的局限性。父类和子类一般是is-a的关系,强调的是本质类型是一样的,接口使得实现代码重用拓展功能从类与类的关系抽离出来。把不是一类的却又相同行为特征的对象抽离封装出来,让类去实现它。
接口的使用:
java是面向对象的,意味着我们要通过类去new对象,所以必须要把接口转换成类的层面上去使用才可以(通过"对象.东西"的方式调功能),故通过implements关键字来让类去接收接口的结构并限制必须是实现接口的抽象方法
接口的使用
下面通过还是通过代码去学习接口的使用和知识点:
/*
* 接口的使用
* 1.接口使用interface来定义
* 2.Java中,接口和类是并列的两个结构
* 3.如何定义接口:定义接口中的成员
*
* 3.1 JDK7及以前:只能定义全局常量和抽象方法
* >全局常量:public static final的.但是书写时,可以省略不写
* >抽象方法:public abstract的
*
* 3.2 JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法(略)
*
* 4. 接口中不能定义构造器的!意味着接口不可以实例化
*
* 5. Java开发中,接口通过让类去实现(implements)的方式来使用.
* 如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化
* 如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类
* 注意:严格来说通常覆盖抽象方法这件事我们叫做实现,而不是重写
*
* 6. Java类可以实现多个接口 --->弥补了Java单继承性的局限性
* 格式:class AA extends BB implements CC,DD,EE
*
* 7. 接口与接口之间可以继承,而且可以多继承
*
* *******************************
* 8. 接口的具体使用,体现多态性
* 9. 接口,实际上可以看做是一种规范
*
* 面试题:抽象类与接口有哪些异同?
*
*/
public class InterfaceTest {
public static void main(String[] args) {
System.out.println(Flyable.MAX_SPEED);
System.out.println(Flyable.MIN_SPEED);
// Flyable.MIN_SPEED = 2;
Plane plane = new Plane();
plane.fly();
}
}
//接口使用interface来定义,Java中,接口和类是并列的两个结构
interface Flyable{
//全局常量
public static final int MAX_SPEED = 7900;//第一宇宙速度
int MIN_SPEED = 1;//省略了public static final
//java会认为他还是public static final
//抽象方法
public abstract void fly();
//省略了public abstract
void stop();
//Interfaces cannot have constructors接口中不能定义构造器,意味着接口不可以实例化
// public Flyable(){
//
// }
}
interface Attackable{
void attack();
}
//Java开发中,接口通过让类去实现(implements)的方式来使用.
//如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化
class Plane implements Flyable{
//实现类实现接口中的抽象方法
@Override
public void fly() {
System.out.println("通过引擎起飞");
}
@Override
public void stop() {
System.out.println("驾驶员减速停止");
}
}
// 如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类
abstract class Kite implements Flyable{
//只实现了一个抽象方法,没有实现全,把它定义为一个抽象类
@Override
public void fly() {
}
}
//类可以实现多个接口--实现一个接口就有了相应的一些功能
class Bullet extends Object implements Flyable,Attackable,CC{
@Override
public void attack() {
}
@Override
public void fly() {
}
@Override
public void stop() {
}
//因为CC接口继承了AA、BB所以实现类也需要实现它们的方法
@Override
public void method1() {
}
@Override
public void method2() {
}
}
//************************************
interface AA{
void method1();
}
interface BB{
void method2();
}
//接口与接口之间可以继承,而且可以多继承
interface CC extends AA,BB{
}
怎么理解:接口是一种规范?
如图是我们现实中电脑的USB接口,其实它就是一个规范,许多设备(如U盘打印机等等)都有USB接口,这些设备都可以通过USB接口来跟电脑交互传输数据。实际上这个接口就是一种规范,该口的长宽高、传输速度、里面有几个针孔等等这些最初就定义好了,类比于Java中接口的概念这些长宽高等属性在接口中就定义为常量
只要是USB口就具有如传输数据、开启传输数据以及结束等等功能,这些功能在不同设备上传输的方式都不一样的。所以只能在接口中定义了一些抽象的方法这样的方法实际上不就是一种规范吗?(这就跟java的接口中定义的抽象方法一样)它定义了数据传输的规范,凡是想要用USB接口的设备就需要按规范去实现自己的传输方法----这些实现类的集合我们就叫做驱动(为什么说是实现类集合?因为你不可能把所有的东西都写到一个类中,接口也会有很多,实现类(实现这些接口(规范)的类也可能会有许多))
举例:把硬盘插到电脑会显示需要我们安装驱动,安装驱动实际上就是我们的USB接口定义了一些规范,现在你想要拿着这个设备来给电脑传输数据了就需要先把实现接口的实现类给下载并跑起来才行!
可以同个一个代码示例来交接下接口的使用:
/*
* 接口的使用
* 1.接口使用上也满足多态性(自己没构造器对象也造不了,要用的话也只能通过多态的方式去使用)
* 2.接口,实际上就是定义了一种规范
* 3.开发中,体会面向接口编程!
*
*/
public class USBTest {
public static void main(String[] args) {
Computer com = new Computer();
//1.创建了接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
com.transferData(flash);//体现了接口的多态性
//2. 创建了接口的非匿名实现类的匿名对象
com.transferData(new Printer());
//3. 创建了接口的匿名实现类的非匿名对象
USB phone = new USB(){
@Override
public void start() {
System.out.println("手机开始工作");
}
@Override
public void stop() {
System.out.println("手机结束工作");
}
};
com.transferData(phone);
//4. 创建了接口的匿名实现类的匿名对象
com.transferData(new USB(){
@Override
public void start() {
System.out.println("mp3开始工作");
}
@Override
public void stop() {
System.out.println("mp3结束工作");
}
});
}
}
class Computer{
//电脑传输数据功能接收的是遵循USB规范的传输设备的对象---多态的体现
public void transferData(USB usb){//USB usb = new Flash(); 接口的多态
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
//定义与电脑传输的规范--谁要是想和电脑传输数据就要实现这个接口把接口中的规范去明确出来
//明确的意思就是拿到全局常量,实现抽象方法
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
//U盘
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}
项目的需求是多变的,我们必须以不变应万变才能从容开发,此处的“不变”就是“规范“。因此,我们开发项目往往是面向接口(规范)编程!
拓展:看两个常见的排错题(笔试题)
//题目一
interface A {
int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A {
public void pX() {
//编译不通过。因为x是不明确的
// System.out.println(x);
//ps:该报错只针对于属性,对于方法有类优先原则
System.out.println(super.x);//1
System.out.println(A.x);//0 ---接口中的x是全局常量直接调就完事
}
public static void main(String[] args) {
new C().pX();
}
}
抽象类和接口的区别
Java8中接口的新特性
JDK7及以前:只能定义全局常量和抽象方法。在JDK8中更新了接口的特性,除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法
/*
*
* JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法
*
*/
public interface CompareA {
//静态方法
public static void method1(){
System.out.println("CompareA:广东");
}
//默认方法
public default void method2(){
System.out.println("CompareA:上海");
}
//可以省略public
default void method3(){
System.out.println("CompareA:上海");
}
}
静态方法可以通过接口直接去调用。默认方式需要通过实现类实例化后通过”实例.方法“的方式去调用,可以直接当作实现类中的普通非静态方法。
基于上边的示例我们看一下怎么去调用接口的静态方法和默认方法已经java8接口新特性各种知识点:
public class SubClassTest {
public static void main(String[] args) {
SubClass s = new SubClass();
// s.method1();//报错:实现类对象完全拿不到接口中的静态方法
// SubClass.method1();报错:实现类完全拿不到接口中的静态方法
//知识点1:接口中定义的静态方法,只能通过接口来调用。
CompareA.method1();
//知识点2:通过实现类的对象,可以调用接口中的默认方法。
//如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法
s.method2();
//知识点3:如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,
//那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。-->类优先原则
//知识点4:如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,
//那么在实现类没有重写此方法的情况下,报错。-->接口冲突。
//这就需要我们必须在实现类中重写此方法
s.method3();//SuperClass:北京,输出的是父类的方法
}
}
//定义个父类
class SuperClass {
//定义一个与接口A的默认方法重名的方法,思考调用时会怎么样?
public void method3(){
System.out.println("SuperClass:北京");
}
}
//定义个接口B
interface CompareB {
//定义一个与接口A的默认方法 重名的方法,思考调用时会怎么样?
default void method3(){
System.out.println("CompareB:上海");
}
//如果实现类没有继承的父类中同名名参数的方法或重写方法,就会报错
//因为编译器分不清到底调取的是A接口还是B接口的method3方法---接口冲突
}
class SubClass extends SuperClass implements CompareA,CompareB{
//实现类重写了接口中的默认方法
public void method2(){
System.out.println("SubClass:上海");
}
public void method3(){
System.out.println("SubClass:深圳");
}
//知识点5:如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
public void myMethod(){
method3();//调用自己定义的重写的方法
super.method3();//调用的是父类中声明的
//调用接口中的默认方法
CompareA.super.method3();
CompareB.super.method3();
}
}
可以看到java8新特性出来后,接口变得越来越像类了。其实我觉得有点失去了”接口就是一种规范的味道了“
类的成员之五:内部类
内部类作为五大类的成员之一,在开发中其实用到的并不多。所以先简单了解下知识点:
当一个属性无法简单描述类的特征的时候就可以使用内部类这种复杂的结构去描述一个类的特征!例如人的大脑比较复杂,不能像年龄和属性一样用普通类型变量甚至字符串足以完全描述,而把大脑单独造成一个类的话因为只需供给人类使用其他类不需要用到那么是有点浪费的。这个时候就可以把它定义为一个内部类。
还是通过代码去看一下:
/*
* 类的内部成员之五:内部类
* 1. Java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类
*
* 2.内部类的分类:成员内部类(静态、非静态) vs 局部内部类(方法内、代码块内、构造器内)
*
* 3.成员内部类:
* 一方面,作为外部类的成员:
* >调用外部类的结构
* >可以被static修饰,注:外部类不能用static修饰(static主要体现的是随着类的加载而加载,就是加载外部类)
* >可以被4种不同的权限修饰
*
* 另一方面,作为一个类:
* > 类内可以定义属性、方法、构造器等
* > 可以被final修饰,表示此类不能被继承。言外之意,不使用final,就可以被继承
* > 可以被abstract修饰,表示此类不能被实例化
*
*
* 4.关注如下的3个问题
* 4.1 如何实例化成员内部类的对象
* 4.2 如何在成员内部类中区分调用外部类的结构
* 4.3 开发中局部内部类的使用
*
*/
public class InnerClassTest {
public static void main(String[] args) {
//如何实例化成员内部类的对象
//创建Dog实例(静态的成员内部类):
Person.Dog dog = new Person.Dog();
dog.show();
//创建Bird实例(非静态的成员内部类):
// Person.Bird bird = new Person.Bird();//错误的,非静态结构必须要实例化后使用,即需要先实例化外部类
Person p = new Person();
Person.Bird bird = p.new Bird();
bird.sing();
System.out.println();
bird.display("黄鹂");
}
}
class Person{
String name = "小明";
int age;
public void eat(){
System.out.println("人:吃饭");
}
//静态成员内部类
static class Dog{
String name;
int age;
public void show(){
System.out.println("卡拉是条狗");
// eat();//静态成员内部类不能调非静态方法
}
}
//非静态成员内部类
class Bird{
//类内可以定义属性、方法、构造器等
String name = "杜鹃";
public Bird(){
}
public void sing(){
System.out.println("我是一只小小鸟");
Person.this.eat();//调用外部类的非静态属性
eat();
System.out.println(age);
}
//如何在成员内部类中区分调用外部类的结构
public void display(String name){
System.out.println(name);//方法的形参
System.out.println(this.name);//内部类的属性
System.out.println(Person.this.name);//外部类的属性
}
}
public void method(){
//局部内部类
class AA{
}
}
{
//局部内部类
class BB{
}
}
public Person(){
//局部内部类
class CC{
}
}
}
开发中局部内部类的使用:
public class InnerClassTest1 {
//开发中很少见
public void method(){
//局部内部类
class AA{
}
}
//返回一个实现了Comparable接口的类的对象
public Comparable getComparable(){
//创建一个实现了Comparable接口的类:局部内部类
//方式一:
// class MyComparable implements Comparable{
//
// @Override
// public int compareTo(Object o) {
// return 0;
// }
//
// }
//
// return new MyComparable();
//方式二:匿名实现类的匿名对象
return new Comparable(){
@Override
public int compareTo(Object o) {
return 0;
}
};
}
}
注意1:局部内部类使用的一个注意点:
在局部内部类的方法中,如果调用局部内部类所声明的方法中的局部变量的话,要求此局部变量声明为final(这是因为方法执行完后变量就没了,为了让类还能使用实际上会给类穿一个副本,这个副本就不能修改)。看起来有点绕,我们看看代码示例:
public class InnerClassTest {
/*
* 在局部内部类的方法中(比如:show)如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,
* 要求此局部变量声明为final的。
*
* jdk 7及之前版本:要求此局部变量显式的声明为final的
* jdk 8及之后的版本:可以省略final的声明
*
*/
public void method(){
//局部变量
int num = 10;
class AA{
public void show(){
// num = 20;
System.out.println(num);
}
}
}
}
注意2:成员内部类和局部内部类,在编译以后,都会生成字节码
格式:
- 成员内部类:
外部类$内部类.class
- 局部内部类:外部类$数字 内部类.class (数字区分可能在不同的类成员中有同名方法)
至此java面向对象篇的内容就结束了,通过基础篇和面向对象篇的学习我们已经能基本上看懂所有的Java代码,剩下的Java内容实际上就是在这上面的基础上去学习Java生态提供给我们一些API的使用。
后面的JavaSE的学习将会分多篇文章挑选开发中比较常用的内容(如多线多进程、反射、常用类等等)去介绍。关于面向对象的知识点的学习大致就是上述的全部,如有纰漏和错误望指出。感谢学习者!谢谢~
本笔记记录的学习资源来自于尚硅谷_宋康红老师的学习资料,讲的特别好!强烈推荐