知识点
- 类
- 对象
- 构造方法
- 引用与对象实例
- static
- final
- 封装
- 继承
- 方法重载与重写
- 多态
- 抽象类
- 接口
- 内部类
- 值传递和引用传递
- 局部变量/方法参数
- 包package
- 类作为形参和返回值
对象
面向对象,从字面意思来看就是我们人面对着一个对象。其实就是指我们从这个对象的整体出发去看它,它由哪些部件组成,它可以做到哪些事情。
类
是一个对象,小明是一个对象。而每个人虽然不同,但却有许多相同的属性和行为,于是我们可以把他们抽象出来,变成一个类,比如人类。
类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称为类。
由此可以总结出类的定义:
- 类是相同或相似对象的一种抽象,是对象的一个模板,它描述一类对象的行为和状态。
- 类是具有相同属性和方法(行为)的对象的集合
**属性是对象具有的特征。每个对象的每个属性都拥有特定值。我们上面讲过对象是一个具体并且确定的事物,正是对象属性的值来区分不同的对象,比如我们可以通过一个人的外貌特征区分他。
那什么是对象的行为呢?在计算机中我们通过方法去实现对象的行为,而对象的方法便是对象所具有的操作,比如人会走路、会哭泣、会学习等等都是人的行为,也就是人的方法。
方法
3、编写类的方法。方法也是写在大括号里面。可以定义一个方法或多个方法,当然也可以不定义方法。
一个类可以包含以下类型变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:也叫静态变量,类变量也声明在类中,方法体之外,但必须声明为 static 类型。
静态变量和成员变量
所属不同
1,静态变量属于类,也成为类变量
2,成员变量属于对象,所以也成为实例变量
内存中位置不同
1,静态变量存储于方法区的静态区
2,成员变量存储于内存
内存出现时间不一样
1,静态变量随着类的加载而加载,随着类的消失而消失
2,成员变量随着对象的创建而存在,随着对象的消失而消失
调用不同
1,静态变量可以通过类名调用,也可以通过对象调用
2,成员变量只可以通过对象名调用
对象
类名 对象名 = new 类名();
比如对 People这个类,我想实例化LiLei这个人。LiLei 的数据类型便是 People 这个类型。(类可以看成使我们自己定义的数据类型)
(感觉有点像C语言的,Struct 类的方法,就是操控struct的函数)
定义类的时候不会为类开辟内存空间,但是一旦创建了对象,系统就会在内存中为对象开辟一块空间,用来存放对象的属性值和方法。
在使用时注意,成员变量可以被本类的所有方法所使用,同时可以被与本类有关的其他类所使用。而局部变量只能在当前的方法中使用。
在这里我们要讲到一个关于作用域的知识了。作用域可以简单地理解为变量的生存期或者作用范围,也就是变量从定义开始到什么时候消亡。
- 局部变量的作用域仅限于定义它的方法内。而成员变量的作用域在整个类内部都是可见的。
- 同时在相同的方法中,不能有同名的局部变量;在不同的方法中,可以有同名的局部变量。
- 成员变量和局部变量同名时,局部变量具有更高的优先级。 大家可以编写代码验证一下。
**构造方法**
创建类的时候需要附带的参数。
比如在新建一个对象 new Object(),括号中没有任何参数,代表调用一个无参构造方法(默认构造方法就是一个无参构造方法)。构造方法的名称必须与类名相同,一个类可以定义多个构造方法。
相当于在创建对象的时候直接赋值。
类的初始化过程
Student s = new Student();
加载Student.class文件进内存
在栈内存为s开辟内存空间
在堆内存为学生对象开辟空间
- 对学生对象的成员变量进行默认初始化
- 对学生对象的成员变量进行显示初始化
- 通过构造方法对学生对象的成员变量赋值
- 学生对象初始化完毕,把对象地址赋值给s变量
引用与对象实例
在新建对象时,需要为对象设定一个对象实例的名称,称为实例名。
这里的object只是Object的引用,什么意思呢,就是object只是指向Object的指针。
静态成员Static
Static关键字注意事项
1.对象
static对象是随着类的创建而创建的,所以无需用this来访问。static修饰的对象的内存分配是在堆中的,不是在栈中的。栈中存储的内容是临时的,是随着方法调用而产生,方法结束而消亡的。
2.方法
static修饰的方法可以直接访问,没有被static修饰的方法需要通过创建对象来访问。
A: 在静态方法中是没有this关键字的
静态是随着类的加载而加载,this是随着对象的创建而 存在的 → 静态比对象先存在
B: 静态方法只能访问静态的成员变量和静态的成员方法
静态方法:
A:成员变量:只能访问静态变量
B:成员方法:只能访问静态成员方法
静态方法是随着类的加载而存在的,静态是优于对象存在的,你要访问非静态的东西,可是这个时候他还不存在。
Java 中被 static 修饰的成员称为静态成员或类成员。它属于整个类所有,而不是某个对象所有,即被类的所有对象所共享。
public static String string = “shiyanlou”);
StaticTest.String第一个函数中可以直接调用StaticTest.String,,说明静态成员归整个类所有
StaticTest staticTest = new StaticTest();这里创建了一个新的staticTest成员(如果不加static,说明string变量并不归整个类所有,要新建一个实例,才可以使用。
需要有一个指向类的指针,创建实例相当于开辟内存空间。。
Final
final 关键字可以修饰类、方法、属性和变量
- final 修饰类,则该类不允许被继承,为最终类
- final 修饰方法,则该方法不允许被覆盖(重写)
- final 修饰属性:则该类的属性不会进行隐式的初始化(类的初始化属性必须有值)或在构造方法中赋值(但只能选其一)
- final 修饰变量,则该变量的值只能赋一次值,即常量
权限修饰符
代码中经常用到 private 和 public 修饰符,权限修饰符可以用来修饰属性和方法的访问范围
如图所示,代表了不同的访问修饰符的访问范围,比如 private 修饰的属性或者方法,只能在当前类中访问或者使用。默认 是什么修饰符都不加,默认在当前类中和同一包下都可以访问和使用。protected 修饰的属性或者方法,对同一包内的类和所有子类可见。public 修饰的属性或者方法,对所有类可见。
我们可以举一个例子,比如 money,如果我们用 private 修饰代表着这是私有的,只能我自己可以使用。如果是 protected 代表着我可以使用,和我有关系的人,比如儿子也可以用。如果是 public 就代表了所有人都可以使用。
客户端程序员:即在其应用中使用数据类型的类消费者,他的目标是收集各种用来实现快速应用开发的类。
类创建者:即创建新数据类型的程序员,目标是构建类。
访问控制存在的原因:
a、让客户端程序员无法触及他们不应该触及的部分 ;
b、允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员
java的四个关键字:public、protected、default、private
(他们决定了紧跟其后被定义的东西可以被谁使用)
适用范围<访问权限范围越小,安全性越高>
在同包子类中1不可以被访问。
private只有一开始的自己能访问
不同包下的子类只能访问后两种,默认是只能访问同一包下的。
封装
封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别
这样做有什么好处?
- 只能通过规定的方法访问数据。
- 隐藏类的实例细节,方便修改和实现。
把height变量定义为private 说明只有People的类才能访问 height
在NewObject类中就无法访问height这个属性,因为不可见
需要使用getHeight()和setHeight()方法去对height进行修改。
补充
封装:
隐藏对象的属性和实现细节,仅对外提供公共访问方式。
封装的好处:
1,隐藏了实现的细节,提供了公共的访问方式
2,提高了代码的复用性
3,提高了安全性
封装的话,如果外部需要对对象的属性进行修改,需要通过类提供的公共的访问方式进行,相当于用类的方法对类进行修改,这就可以在对方法输入参数的时候,对输入的参数进行判断,判断是否符合对象的属性修改逻辑。
比如说,我要对StudentDemo测试类进行封装,因为我们只允许对测试的对象,进行创建和调用方法两种操作。不允许更改其内部的属性,这时候我们就需要进行封装。
这种情况下,java为我们提供了private关键字。
this
this 关键字代表当前对象。使用 this.属性 操作当前对象的属性,this.方法 调用当前对象的方法。
用 private 修饰的属性,必须定义 getter 和 setter 方法才可以访问到 (Eclipse 和 IDEA 等 IDE 都有自动生成 getter 和 setter 方法的功能)。
继承
例如我们定义了一个 Animal 类,再创建一个 Dog 类,我们需要它继承 Animal 类。
为什么需要继承?
如果有两个类相似,那么它们会有许多重复的代码,导致后果就是代码量大且臃肿,后期的维护性不高。通过继承就可以解决这个问题,将两段代码中相同的部分提取出来组成一个父类,实现代码的复用。
继承的特点:
- 子类拥有父类除 private 以外的所有属性和方法。
- 子类可以拥有自己的属性和方法。
- 子类可以重写实现父类的方法。
- Java 中的继承是单继承,一个类只有一个父类。
- 在子类方法中访问一个变量——子类局部范围查找→子类成员范围查找→父类成员范围
①单继承
子类会继承父类的属性、方法,且只能继承一个父类的模板,这个叫单继承。
②多继承
每个类可以同时继承多个父类,叫做多继承。
多继承的优点:
1,可以同时具备来自多个父类的特征,让子类有更大的丰富度
多继承的缺点:
1,如果多个父类具有相同的特征,那么子类到底要继承哪一个,这会产生混乱
2,多继承会让清晰的树状结构变成复杂的网状结构。
但是Java中并不能实现多继承,这时候我们可以通过,接口和内部类来实现多继承的功能。
在继承时,创建对象的,内存叠加模型
创建对象时,java是从最高的父类开始创建,一级级往下创建,最后创建一个具有各个父类的属性的对象。
与继承有关的知识点: 父类的构造方法不会被子类继承 1,构造方法语法上要求方法名和类名保持一致 2,构造方法是用来产生对象的,如果子类通过继承拥有了父类的构造方法,产生了父类的对象这样是会起冲突的
父类的属性会被子类继承,无论访问修饰符!
继承机制中已经讲了,产生子类对象会先产生父类对象的部分,然后叠加子类特有的部分,从而构成完整的子类对象。所有父类的属性都会被放入到子类当中。
父类的private属性会被继承,但无法访问!
private修饰符其本意为:私有。父类如果把该属性定义为了private,即意味着该属性只能让父类自己在内部直接访问;而子类从语意上就不应该能够访问它(没有得到父类的允许)。
super
super 关键字在子类内部使用,代表父类对象。
- 访问父类的属性 super.属性名。
- 访问父类的方法 super.bark()。
- 子类构造方法需要调用父类的构造方法时,在子类的构造方法体里最前面的位置:super()。
this(),super(),this 调用本类的构造方法,super()调用父类的构造方法
方法重载与重写
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。方法重载一般用于创建一组任务相似但是参数不同的方法。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/424138/1624843238949-b988516a-66a0-4ee8-9b69-c9a694c4407c.png#clientId=u24beba3f-9006-4&from=paste&height=420&id=u4eff14c2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=840&originWidth=589&originalType=binary&ratio=2&size=43685&status=done&style=none&taskId=ua565e7fd-05ab-4014-908a-835d7fed145&width=294.5)
相当于一个函数有了很多不同类型的接口
方法重载有以下几种规则:
- 方法中的参数列表必须不同。比如:参数个数不同或者参数类型不同。
- 重载的方法中允许抛出不同的异常
- 可以有不同的返回值类型,但是参数列表必须不同。
- 可以有不同的访问修饰符。
方法重写
子类可以继承父类的方法,但如果子类对父类的方法不满意,想在里面加入适合自己的一些操作时,就需要将方法进行重写。并且子类在调用方法中,优先调用子类的方法。
比如 Animal 类中有 bark() 这个方法代表了动物叫,但是不同的动物有不同的叫法,比如狗是汪汪汪,猫是喵喵喵。
当然在方法重写时要注意,重写的方法一定要与原父类的方法语法保持一致,比如返回值类型,参数类型及个数,和方法名都必须一致。
多态
多态是指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。多态也称作动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
通俗地讲,只通过父类就能够引用不同的子类,这就是多态,我们只有在运行的时候才会知道引用变量所指向的具体实例对象。
多态的实现条件
Java 实现多态有三个必要条件:继承、重写和向上转型(即父类引用指向子类对象)。
只有满足上述三个条件,才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
多态的实现方式
Java 中多态的实现方式:继承父类进行方法重写,抽象类和抽象方法,接口实现。
多态访问成员方法,编译看左边,但是运行看右边。
虽然我们在内存中定义的对象是猫,但是外界看到的其实是Animal
相当于是用Cat(),给Animal赋值,但是Animal没有Weight变量
多态访问成员变量时,编译看左边,运行也看左边。
对象看左边,方法也是看左边
多态就是,一个类在不同情况下,做出不同反应的意思
比如说,Animal类有时候可以是猫,有时候可以是狗……
这里的Animal可以根据输入的情况,自动变成狗或者猫
这里只是用Cat和Dog给Animal赋值,但是Animal中没有的属性是无法赋值的。
多态中的转型
1,向上转型
从子到父
父类引用指向子类对象
2,向下转型
从父到子
父类引用转为子类对象
这里创建了一个动物类,a 可以用猫给动物类赋值,说明动物可以变成猫
但是后面a之后被dog类赋值了,这时候我们不能把a强制转型为Cat类
虽然a是动物类,其实里面装的是dog的东西,只是Animal类无法表示dog,但是不能用dog给cat赋值
多态转型内存图解
向上转型
在这里,可以认为由于 Dog 继承于 Animal,所以 Dog 可以自动向上转型为 Animal,所以 b 是可以指向 Dog 实例对象的。
注:不能使用一个子类的引用去指向父类的对象,因为子类对象中可能会含有父类对象中所没有的属性和方法。
如果定义了一个指向子类对象的父类引用类型,那么它除了能够引用父类中定义的所有属性和方法外,还可以使用子类强大的功能。但是对于只存在于子类的方法和属性就不能获取。
在这里,由于 b 是父类的引用,指向子类的对象,因此不能获取子类的方法(dogType() 方法), 同时当调用 bark() 方法时,由于子类重写了父类的 bark() 方法,所以调用子类中的 bark() 方法。
animal中有bark方法,但没有dogTpye方法
所以b可以调用bark但不可以调用dogTpye
抽象类
抽象方法一定在抽象类中,抽象类不一定只能有抽象的方法
抽象类要通过继承来创建方法
在定义类时,前面加上 abstract 关键字修饰的类叫抽象类。
抽象类中有抽象方法,这种方法是不完整的,仅有声明而没有方法体。抽象方法声明语法如下:
在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确知道这些子类如何实现这些方法。也就是说抽象类是约束子类必须要实现哪些方法,而并不关注方法如何去实现。- 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免了子类设计的随意性。
所以由上可知,抽象类是限制规定子类必须实现某些方法,但不关注实现细节。
那抽象类如何用代码实现呢,它的规则如下:
- 用 abstract 修饰符定义抽象类。
- 用 abstract 修饰符定义抽象方法,只用声明,不需要实现。
- 包含抽象方法的类就是抽象类。
- 抽象类中可以包含普通的方法,也可以没有抽象方法。
- 抽象类的对象不能直接创建,通常是定义引用变量指向子类对象。
接口
接口用于描述类所具有的功能,而不提供功能的实现,功能的实现需要写在实现接口的类中,并且该类必须实现接口中所有的未实现方法。
接口可以声明引用,但不可以声明对象
接口类的成员变量是常量并且是被static修饰的,可以直接访问。
类和接口的关系
类和类的关系
继承关系,只能单继承,但是可以多层继承
类和接口的关系
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。
案例分析
内部类
将一个类的定义放在另一个类的定义内部,这就是内部类。而包含内部类的类被称为外部类。
内部类的主要作用如下:
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类
- 内部类的方法可以直接访问外部类的所有数据,包括私有的数据
- 内部类所实现的功能使用外部类同样可以实现,只是有时使用内部类更方便
- 内部类允许继承多个非接口类型(具体将在以后的内容进行讲解)
注:内部类是一个编译时的概念,一旦编译成功,就会成为完全不同的两类。对于一个名为 outer 的外部类和其内部定义的名为 inner 的内部类。编译完成后出现 outer.class 和 outer$inner.class 两类。所以内部类的成员变量 / 方法名可以和外部类的相同。
成员内部类的使用方法:
- Student 类相当于 People 类的一个成员变量,所以 Student 类可以使用任意访问修饰符。
- Student 类在 People 类里,所以访问范围在类里的所有方法均可以访问 People 的属性(即内部类里可以直接访问外部类的方法和属性,反之不行)。
- 定义成员内部类后,必须使用外部类对象来创建内部类对象,即 内部类 对象名 = 外部类对象.new 内部类();。
- 如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用 this 关键字。如上述代码中:a.this。
注:成员内部类不能含有 static 的变量和方法,因为成员内部类需要先创建了外部类,才能创建它自己的。
静态内部类
静态内部类是 static 修饰的内部类,这种内部类的特点是:
- 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问。
- 如果外部类的静态成员与内部类的成员名称相同,可通过 类名.静态成员 访问外部类的静态成员;如果外部类的静态成员与内部类的成员名称不相同,则可通过 成员名 直接调用外部类的静态成员。
- 创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名 = new 内部类();。
局部内部类
局部内部类是指类定义在方法和作用域内。
public class People{
public void peopleInfo(){<br /> final String sex = "man";<br /> class Student{<br /> String ID = "20151234";<br /> public void print(){<br /> System.out.println("访问外部类的方法中的常量sex:" + sex);<br /> System.out.println("访问内部类中的变量ID:" + ID);<br /> }<br /> }<br /> Student a = new Student();<br /> a.print();<br /> }<br /> public void peopleInfo2(boolean b){<br /> if(b){<br /> final String sex = "man";<br /> class Student{<br /> String ID = "20151234";<br /> public void print(){<br /> System.out.println("访问外部类的方法中的常量sex:"+ sex);<br /> System.out.println("访问内部类中的变量ID:" + ID);<br /> }<br /> }<br /> Student a = new Student();<br /> a.print();<br /> }<br /> }<br /> public static void main(String[] args){<br /> People b = new People();<br /> System.out.println("定义在方法内:========");<br /> b.peopleInfo();<br /> System.out.println("定义在作用域内:=====");<br /> b.peopleInfo2(true);<br /> }<br />}
局部内部类也像别的类一样进行编译,但只是作用域不同而已,只在该方法或条件的作用域内才能使用,退出这些作用域后无法引用的。
作用域的意思就是,在if{}里面可以用的就叫做作用域
匿名内部类
匿名内部类,顾名思义,就是没有名字的内部类。正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写。但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口。
匿名内部类是不能加访问修饰符的。要注意的是,new 匿名类,这个类是要先定义的, 如果不先定义,编译时会报错该类找不到。
同时,在上面的例子中,当所在的方法的形参需要在内部类里面使用时,该形参必须为 final。这里可以看到形参 name 已经定义为 final 了,而形参 city 没有被使用则不用定义为 final。
Package
为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。
包的作用
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 包采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
使用 import 关键字。比如要导入包 com.shiyanlou 下 People 这个类,import com.shiyanlou.People;。同时如果 import com.shiyanlou.; 这是将包下的所有文件都导入进来, 是通配符。
值传递和引用传递
1,搞清楚“基本类型”和“引用类型”的不同之处
num是基本类型,值就直接存储在变量中
str是引用类型,变量中保存的只是实际对象的地址
2,搞清楚赋值运算符(=)的作用
对于基本类型num,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型str,赋值运算符会改变引用中所保存的地址,原来的值不变,比如说这里“hello”的值没有改变。
但是没有被任何引用指向的对象,会被计算机自动进行垃圾回收
3,调用方法时发生了什么?参数传递本质上就是赋值操作。
这里的操作感觉有点像指针啊
这里方法传入的参数,作用域只在方法内部,如果不直接调用改变传入引用的方法,是不会改变传入参数的值的。
我们这边重点理解一下,第三个和第四个例子的不同之处。
builder.append(“4”);之后
builder = new StringBuilder(“ipad”);之后
局部变量和方法参数
局部变量和方法参数在jvm中的储存方法是相同的,都是在栈上开辟空间来储存的,随着进入方法开辟,退出方法回收。以32位JVM为例,boolean/byte/short/char/int/float以及引用都是分配4字节空间,long/double分配8字节空间。对于每个方法来说,最多占用多少空间是一定的,这在编译时就可以计算好。
我们都知道JVM内存模型中有,stack和heap的存在,但是更准确的说,是每个线程都分配一个独享的stack,所有线程共享一个heap。对于每个方法的局部变量来说,是绝对无法被其他方法,甚至其他线程的同一方法所访问到的,更遑论修改。
当我们在方法中声明一个 int i = 0,或者 Object obj = null 时,仅仅涉及stack,不影响到heap,当我们 new Object() 时,会在heap中开辟一段内存并初始化Object对象。当我们将这个对象赋予obj变量时,仅仅是stack中代表obj的那4个字节变更为这个对象的地址。
数组类型和引用对象
当我们声明一个数组时,如int[] arr = new int[10],因为数组也是对象,arr实际上是引用,stack上仅仅占用4字节空间,new int[10]会在heap中开辟一个数组对象,然后arr指向它。
当我们声明一个二维数组时,如 int[][] arr2 = new int[2][4],arr2同样仅在stack中占用4个字节,会在内存中开辟一个长度为2的,类型为int[]的数组,然后arr2指向这个数组。这个数组内部有两个引用(大小为4字节),分别指向两个长度为4的类型为int的数组。
所以当我们传递一个数组引用给一个方法时,数组的元素是可以被改变的,但是无法让数组引用指向新的数组。
你还可以这样声明:int[][] arr3 = new int[3][],这时内存情况如下图
你还可以这样 arr3[0] = new int [5]; arr3[1] = arr2[0];
Package 包
包其实就是文件夹,作用是对类进行分类管理
格式:package 包名
范例:package com.itheima;
javac -d . HelloWorld.java 自动建包
如果不在同一个包下是不能使用其他包的类的
我们调用的时候应该连同包一起调用。
cn.itcast.Teacher
在使用其他包的东西之前我们应该先导包
import
导入了 cn.itcast包中的Teacher类
类作为形参和返回值
类可以作为传入的参数和返回值
传入之后可以使用类的方法。
抽象类也可以作为传入的参数和返回值
如果只是抽象类是没有具体的方法的,这时候需要用多态的手段给抽象类赋值,之后才可以把抽象类作为形参。