本文主要是来介绍一下Java的面向对象的一些知识点,配套一些代码的练习,让我们能够更加深入的理解Java面向对象的一些特质以及内容,笔者刚刚入门Java,书写本文主要是一者是自己巩固加深学习内容,二者是为了以后入行Java的小伙伴用来入门。因为有很多崇拜的大佬都是这样做的,到我这里自然也应该继续传承下去。
人生百年,吾道不孤。
一.面向对象
面向对象(Object Oriented)很多公司在招聘要求上写的OO就是它啦。那么究竟什么叫面向对象呢?我们举一个栗子。
请问如何通过编程把大象装进冰箱里?
对于面向对象来说这个问题的解决方法是:首先我先从现实世界出发,把大象装进冰箱,一共需要这么几样,首先需要一只冰箱能装下的大象,其次需要一个能装东西的冰箱,最后需要一个东西把大象运送到冰箱里,那么我们就可以清楚的意识到,我如果想要通过编程来解决这个问题,我就要画出来一个冰箱的设计图,画出来一个大象的设计图,再画出来一个把大象装进冰箱的机器或者人的设计图,然后明确这些画出来的东西一些需求所需的功能,比如我们要赋予大象被装进冰箱的功能,赋予冰箱能够装东西的功能,赋予人或者机器能够运送大象的功能。然后按照我的设计图,在程序世界制造出来这么三个东西,然后利用预先设计的功能,把制造出来的大象,真正的送到我们制造的冰箱之中。
初步了解了什么叫面向对象,那么我们来介绍几个面向对象的术语
- 类:对一类事物的描述,是抽象的,概念上的定义。相似于我们上述所说的设计图
- 对象:实际村扎起的该类事物的每个个体,也称为实例。相似于我们在程序世界真正制造出的大象,冰箱等
下面我们来介绍一下面向对象的定义:
面向对象就是强调具备功能的对象,以类/对象为最小单位,考虑如何解决问题。
类和对象的关系:相信大家已经可以很了解类和对象的概念了,其实对象就是类的实例化。
几个补充的概念: - 属性:成员变量 = field = 字段,域
- 方法:成员方法 = method = 函数
- 对象: 类的对象 = 类的实例化 = 实例化类
对象的创建过程和内存解析:
首先来先写一个类及其实例化的代码
public class OOTest {
public static void main(String[] args) {
Person p1 = new Person(17, "张三");
Person p2 = new Person(18, "李四");
p1.name = "Tom";
System.out.println(p2.name);
Person p3 = p1;
System.out.println(p3.name);
}
}
class Person {
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public Person() {
}
public void favoriteFood(String food){
System.out.println("我最爱吃的事物"+food);
}
public void introduce(){
System.out.println("我叫"+name+","+"今年"+age+"岁了");
}
}
然后我们来解析一下类和对象在内存层面是如何建立起来的。
由图我们可以知道,我们如果创建了一个类的多个对象,他们每个都有自己独立了一套类的属性,就好像我们都是人,但每个人都有自己独立的外表,年龄。相互独立,互不干预。也就是说我们修改张三的名字时,李四的名字和年龄都不会受到任何影响。而我们进行赋值的时候,也就是p3=p1的时候,我们是将p3直接指向了p1的内存地址,并不会创建新的对象,而是共用同一个堆空间的对象实体。这里的p3和p1就像是一个人的俩名字一样,小白也是我,王铁蛋也是我,修改王铁蛋的年龄就是修改小白的年龄。(这里我们需要注意的是,static所修饰的变量和方法是不再我们刚刚所述的内容之中的,static我们后面再讲)
对象数组的内存解析:
我们说过,数组是引用数据类型,让我们先来复习一下数据类型的分类:
基本数据类型:整型,浮点型,字符型,布尔型
引用数据类型:数组,类(class),接口(interface)
数组可以是很多类型的,自然可以是对象数组,下面直接看一下定义对象数组的代码:
public class OOTest {
public static void main(String[] args) {
Person[] team = new Person[5];
team[0] = new Person();
team[1] = new Person();
team[0].name="qiqi";
team[1].age = 27;
System.out.println(team[0].name);
}
}
class Person {
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public Person() {
}
public void favoriteFood(String food){
System.out.println("我最爱吃的事物"+food);
}
public void introduce(){
System.out.println("我叫"+name+","+"今年"+age+"岁了");
}
}
我们可以看出,起初我们定义对象数组时,只会进行在堆空间中开辟一块连续的空间,而并不会进行初始化,所以我们要使用数组的时候,首先要对数组进行初始化操作才能正常使用,而且在连续的数组空间中,存放的并不是实例化的类,而是实例化类的地址,实例化类分散在堆空间的不同位置,通过地址调用他们。
匿名对象:有些时候,有些东西可能用了一次之后就不再使用了,如果通过正常的实例化类的方式,可能比较麻烦,而且因为有栈空间指向,所以他不会被垃圾回收,导致堆空间被占用,而匿名对象是没有栈空间指向的,所以使用过一次之后,会立即清理,不会占用堆内存。下面介绍匿名对象的写法:
public class Test2 {
public static void main(String[] args) {
PhoneMall pa1 = new PhoneMall();
pa1.show(new Phone(1700,"三星盖乐世")); //匿名方法
}
}
class Phone{
public Phone(int price, String name) {
this.price = price;
this.name = name;
}
public int price;
public String name;
}
class PhoneMall{
public PhoneMall() {
}
public void show(Phone phone){
System.out.println(phone.name);
System.out.println(phone.price);
}
}
二、类的结构
类的结构之一:属性
属性其实也是类中定义的一个变量,那么它和局部变量有啥相同点和不同点呢?
1.局部变量和属性的相同点
- 定义格式相同 权限修饰符 数据类型 变量名 = 变量值
- 先声明后使用
- 都有其对应的作用域
2.局部变量和属性的不同点 - 在类中的声明位置不一样。
局部变量是在类的方法内,方法的形参中,代码块中,构造器的形参,构造器的内部中声明的变量。而属性是直接定义在类的一对{}中的。 - 关于权限修饰符的不同。
属性可以使用权限修饰符修饰,而成员变量不可以使用权限修饰符。
顺便复习一下权限修饰符的种类和用途。
修饰范围总结:
public > protected > 缺省 > private
- 默认初始化值的情况:
类的属性,根据其属性的类型,都会默认初始化值。而局部变量则不会
初始化值:整型:byte,short,int,long = 0
浮点型:float,double =0.0
字符型:char = 0 ('\u0000')
布尔型:boolean = false
引用数据类型:(类,接口,数组)null
- 内存中加载的位置:
属性:加载到堆空间 (非static)
局部变量:加载到栈空间
类的结构之二:方法
方法就是类似于函数,他的作用是描述类应该具有的功能。
声明方式:
权限修饰符 返回值类型 方法名(形参列表){
方法体
}
public void test1(String name1,String name2,String name3){
}
- 权限修饰符:
方法的权限修饰符只有public,Java规定的4种权限修饰符都能够使用。 - 返回值类型:
如果有返回值的话,必须在方法声明时,指定返回值的类型,同时在方法体内,需要使用return关键字来返回指定类型的变量或常量
如果没有返回值的话,就不需要使用return,如果想使用return的话,就是能使用return;表示方法的结束 - 方法名:
方法名的定义首先要做到见名知意,其次需要遵循规范首单词的字母小写,后续的单词都大写。举个例子,就是满足如下 xxxYyyZzz()这样的格式。 - 形参列表:
方法中可以有任意多个形参,定义格式如上面代码所示。 - 方法体
方法功能的实现位置。 - 注意点
方法内可以调用方法,方法内不能定义方法
类的结构之三:构造器
所谓构造器,就是用于实例化对象的,也就是用来创建对象和初始化的。
构造器的说明:
1.如果没显示的定义类的构造器的话,系统会默认提供一个空参的构造器
2.定义构造器的格式:权限修饰符 类名(形参列表){}
3.一个类中定义的多个构造器,彼此构成重载
4.一旦我们显式的定义类的构造器,系统就不再提供默认的空参构造器了
5.一个类中,至少需要一个构造器
之前我们说过,权限修饰符可以修饰方法,属性,类,构造器。
需要注意的是,类只可以使用public和缺省来修饰,其他三类则是都可以使用。
类的结构之四:代码块
- 代码块的作用:代码块主要是用来初始化类,对象的信息。
代码块可以分为静态代码块和动态代码块,静态代码块是用static修饰的代码块,非静态代码块是不用static修饰的代码块。
静态代码块:
内部可以输出语句,随着类的加载而执行,而且只执行一次,可以初始化类的信息,如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行,静态代码块优先于非静态代码块的执行,静态代码块只能够调用静态的属性,静态的方法,不能够调用非静态的结构。
public class Test2 {
public static void main(String[] args) {
Phone phone1 = new Phone(500, "小米");
Phone phone2 = new Phone(10000, "苹果");
}
}
class Phone{
public Phone(int price, String name) {
this.price = price;
this.name = name;
}
static
{
int a = 50;
System.out.println("类要启动了偶");
}
public int price;
public String name;
}
由本程序的输出结果可以看到,静态代码块是随着类的加载而运行的,而类在一个程序之中,只会在第一次实例化对象的时候加载到内存中,后续实例化对象时,则不必再次加载类,这也就是为什么代码块中的内容只会执行一次的原因。
非静态代码块:
内部可以输出语句,随着对象的创建而执行,每创建一个对象,就执行一次非静态代码块 可以在执行对象时,对对象的属性进行初始化,也可以调用静态的属性方法,以及非静态的属性方法。
public class Test2 {
public static void main(String[] args) {
Phone phone1 = new Phone("小米");
Phone phone2 = new Phone(10000, "苹果");
System.out.println(phone1);
}
}
class Phone{
public Phone(int price, String name) {
this.price = price;
this.name = name;
}
public Phone(String name) {
this.name = name;
}
{
System.out.println("芜湖");
this.price = 66;
}
static
{
int a = 50;
System.out.println("类要启动了偶");
}
public int price;
public String name;
@Override
public String toString() {
return "Phone{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
}
根据输出结果大家可以发现,对于非静态代码类来说,每次实例化类的时候,都会执行一次非静态代码块的内容,而且无论先后顺序,都会在静态代码块后执行,可以给实例化类的对象赋值,可以通过这个方法设定默认值。
实例化子类对象时,涉及到父类静态代码块,子类静态代码块,非静态代码块,构造器的加载顺序为:
由父及子,静态现行
三、面向对象的三大特征
1.封装性
- 为什么引入封装性,封装性的意义是什么?
一般来说,我们设计程序追求的是高内聚,低耦合,也就是类的内部数据细节自己完成,不允许外部进行干涉,低耦合是指仅对外暴露少量的方法用于使用,这样做的一个好处是安全,不让外部类直接随心所欲的操作我的类的内部数据,而是如果你想用我类中内部的数据,就必须要通过我的方法,按照我的规则去操控,只有内部如何实现你不用知道,你只要知道你给我三块钱就有一瓶肥宅快乐水就可以了,但是肥宅快乐水咋来的,钱又哪去了你就不用知道了。这就是安全性。还有一个好处就是提高代码的重用性,我把他封装成了一个方法,就比如不止有这一个贩卖机给他三块钱就给你一瓶肥宅快乐水,所有的商店都能使用,这时候我们就可以通过调用这个方法放在每一个商店,不用自己重写了。会极大的降低代码的复杂度。还有就是方便管理,Java是提供Javadoc功能的,他可以对于你所写的类以及方法进行简述,这样的话,别人拿到你的类,就知道这个类有哪些封装好的函数可以使用,方便使用。 - 封装性如何体现在代码中的呢?
- 体现一:将类的属性私有化,通过public的get和set方法使用
- 体现二: 不对外暴露私有的方法
- 体现三: 单例模式
- 体现四:如果不希望类在包外被调用,可以设置位缺省的。
//饿汉式单例模式
class ATM{
private long deposit;
private String name;
private static ATM instance = new ATM(100000,"ATM机");
private ATM(long deposit, String name) {
this.deposit = deposit;
this.name = name;
}
public static ATM getInstance(){
return instance;
}
}
#线程安全的懒汉式
class Bank{
private Bank(){
}
private static Bank instance = null;
public static Bank getInstance(){
if(instance == null){
synchronized (Bank.class){
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
2.继承性
为什么要使用继承性?
使用继承性也是主要为了减少代码的冗余,能够让代码更加的简洁轻便,提高代码的复用性,同时便于功能的拓展为多态性的使用提供了前提。
什么叫做继承性?
所谓继承性,就是如果有了这种继承关系,相当于子类拥有了父类全部的方法和属性,即使是权限修饰符为private的属性和方法,子类都可以继承,只是无法调用而已。而且在这种前提下,子类还可以拥有属于自己的方法和结构,也可以对父类的方法进行重写,实现功能的拓展。
Java中继承性的说明:
1.继承性是一对多的,也就是子类只有一个父类,而父类可以拥有很多子类
2.子父类是相对概念,也就是这个类的父类,可能是两一个类的子类
3.子类直接继承的父类,称为直接父类,父类的父类称为间接父类。
4.子类继承父类之后,会获取直接父类和间接父类所有的属性和方法。
5.java.lang.Object类是所有类的父类
继承性的使用格式和方法:
class People{
private int age;
private String name;
public People() {
}
public People(int age, String name) {
this.age = age;
this.name = name;
}
public void show(){
System.out.println("我是人 我吃饭");
}
}
class Student extends People{
@Override
public void show() {
System.out.println("我是学生 我吃肉");
}
}
如上述代码所示,就是一个完整的继承性的体现,因为有些时候,父类的方法体中的内容并不适合于子类,所以我们需要做的是,将其中的内容进行修改,从而变成适合于子类的内容,这样我们需要做的就是重写。上面的代码就是一段重写过程。重写以后,在调用子类对象继承父类的同名同参数的方法的时候,使用的就是子类重写父类的方法。
重写的规则:
1.子类重写的方法名和形参列表类型必须和父类被重写的方法名和形参列表一致
2.子类重写方法的权限修饰符不小于父类被重写的权限修饰符(不能重写父类中权限为private的方法)换而言之只能重写到子类能访问到的类
3.子类重写的返回值类型必须和父类返回值类型相同,或者是父类返回值类型的子类
4.子类重写的方法抛出的异常类型不得大于父类抛出的异常类型
5.子类和父类是否为static类型要保持一致
重写和重载的区别:
重载是在同一个类中,有相同的方法名,但是形参列表不同,返回类型可以相同也可以不同,抛出异常大小也不发生关系。
重写是在子类中,对于父类同名同参数的方法进行重写,返回值只能是父类返回值或者其子类,抛出的异常不能大于父类。
重载不表现为多态性。重写表现为多态性
3.多态性
为什么要使用多态性?
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。可以提升代码的通用性。
多态性的使用方式是什么?
有了多态性以后,我们在编译期,是你能调用父类声明的方法,但是在运行期,我们实际执行的是子类重写的方法,也就是编译是时候看左边,运行的时候看右边。
多态性的应用举例:
public class polymorphismTest {
public static void main(String[] args) {
Animal ani = new Dog();
ani.show();
System.out.println(ani.name);
}
}
class Animal{
public String name= "s";
public void show(){
System.out.println("我只是个小动物");
}
}
class Dog extends Animal{
@Override
public void show() {
public String name = "d1wdw";
System.out.println("我是个小狗狗");
}
}
多态性使用的注意点:
对象的多态性,只使用于方法,不会用于属性,编译和运行都看左边
多态性和强制转换:
多态性是一种向上转型,强制类型转换是一种向下转型,就类似于变量的自动类型提升和强制类型转换的关系一样。多态性我们已经进行了概述,下面我们着重介绍一下向下转型。
为什么使用向下转型?
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。如何才能调用子类特的属性和方法?使用向下转型。
如何实现向下转型:使用强制类型转换符()
向下转型的注意点:
使用强转的时候可能会出现ClassCastException的异常,为了避免向下转型出现ClassCastException的异常,我们在向下转型是需要进行instanceof判断,返回true,就进行向下转型,如果不是就不能进行向下转型。
a instanceof A
//判断对象a是否是类A的实例
//要求a所属的类与类A必须是子类和父类的关系