Day10 Java面向对象(上)
OOP
一. 面向对象 & 面向过程
-
面向过程思想:
- 分步解决一个问题。第一步做什么,第二步做什么,直至完成。它是一种线性的思维,面对大型的复杂问题难以解决。
- 他适合处理一些比较简单的问题。
-
面向对象思想:
- 将一个问题抽象分割为几类更小的问题,然后对每个小问题进行单独的思考解决,最终达到解决问题的目的。在对小问题的解决仍然需要用到面向过程的思想。
- 面向对象适合处理复杂问题,适合处理需要多人协作的问题。
-
对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。
二. 什么是面向对象(Object-Oriented Programming, OOP)
-
面向对象的本质:以类的方式组织代码,以对象的组织(封装)数据。
-
三大特性:
- 封装:将数据,操作等封装到类中。
- 继承:类之间的继承关系。
- 多态:事物的多样性。
-
从认识论角度考虑是现有对象后有类。对象是具体的事物。类,是对对象的抽象。
-
从代码运行角度考虑是先有类后有对象。类是对象的模板。
三. 回顾方法与加深
-
回顾方法:
-
静态方法和非静态方法
方法是否是一个静态方法可以用关键字
static
来区分。静态方法,和类一起加载,无需创建实例对象即可直接使用
className.methodName
调用。非静态方法,当类实例化出一个对象时才可以使用,使用
objectName.methodName
调用。以下是一个例子,展示了上述规则,并且演示了静态方法和非静态方法的调用关系:
public class Method { String name = "konoha"; public static void main(String[] args) { hello(); //showName(); 无法调用 //新建Method对象 Method method = new Method(); method.showName(); } public static void hello() { System.out.println("Hello world!"); //showName(); 无法调用非静态方法 } public void showName() { System.out.println("My name is " + name); hello(); //可以直接调用自己的静态方法 } }
-
值传递和引用传递
在Java的方法调用中,参数之间的传递是值传递,就是将值复制一份传给函数使用。但是由于Java的内存管理方式,不同的数据类型在方法中进行修改时,会产生两种不同的结果。一种是数据不变,一种是数据改变。下面的我的解释帮组我们简单了解其中的原因。我们先看两个例子:
public class ValuePassIssue { public static void main(String[] args) { int a = 5; System.out.println(a); changeNum(a); System.out.println("Num after method used: " + a); //仍然输出5 System.out.println("============================="); Test test = new Test(); System.out.println("test name: " + test.name); changeName(test); System.out.println("test name after method uesd: " + test.name); //输出Alice } public static void changeNum(int n) { n = 10; } public static void changeName(Test test) { test.name = "Alice"; } } class Test { String name = "konoha"; }
为什么同样的方法,一个值改变了,一个值没变呢?大家不知道是否还记得在数组时讲过的Java内存分析,忘记的同学可以再去看一看狂神的讲解。这里给一个图:
我们知道,int类型等都是基本变量类型,按照规则他们都储存在栈中。当你将他作为参数传递给方法时,你可以理解为他的值被复制了一份传递给了函数的形式参数,形式参数被实例化为变量参与函数内的运算。而这个变量与原变量没有任何关系,除了他们的值刚开始相同以外。所以自然改变他也不会影响到原来的变量。所以第一个方法修改变量失败了。
而我们将对象传递给方法时,和基本变量一样的过程,我们仍旧是将值传递给了方法的参数使其变成一个和传入变量具有相同值的变量。但是对象存储的是一个引用变量,是对象在堆里的具体位置。做一个比喻,就像这个变量记得是你家的门牌号,不论把他复制几份,拿到了这个门牌号的人就能按照他找的你的家。对象也是如此,引用指向对象在堆中的地址,任何函数拿到这个地址都可以根据他修改堆中的对象。因此第二个方法修改对象的属性成功了。
四. 类的成员
- 属性:
- 实例属性:类实例化出的对象的属性,各个对象互不相关
- 静态属性:类的属性,所有对象共用一个
- 方法:
- 构造器:类创建对象时的构造方法
- 静态方法:无需创建类的实例对象就可以调用的方法
- 实例方法:必须创建类的实例对象后才能调用的方法
五. 类和对象的关系
-
类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物。
- 动物、植物、手机、电脑.....
- Person类、Pet类、Car类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为
-
对象是抽象概念的具体实例。
- 张三就是人的一个具体实例,张三家里的旺财就是狗的一个具体实例。
- 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念.
六. 创建与初始化对象
-
使用new关键字创建对象。
-
使用
new
关键字创建对象的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始以及对类中构造器的调用。 -
类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:
-
必须和类的名字相同。
-
必须没有返回类型,也不能写void。
-
-
细节知识:
new
关键字创建对象实质上就是调用对象的构造函数。- 类的构造函数的作用主要是给对象初始化属性。
- 类的构造函数的本质还是方法,多个不同的构造函数可以共存也是因为方法的重载。
- 类如果不写构造函数会隐式为类添加一个空的构造函数。(可以通过IDEA的反编译class文件查看到)
- 如果类写了带参数的构造函数,那么如果需要空的构造函数必须要显示的声明声明,否则默认没有该方法。
this
关键字就是指代对象自身。- 每个类最好写单独的一个文件。
一个创建对象的实例:
package konoha.oop.objectLearn; //操作类 public class Control { public static void main(String[] args) { Pet dog = new Pet(); //默认初始化的对象 System.out.println("Pet Name: " + dog.name + " , Pet age: " + dog.age); //可以使用'.'操作符操作类中共有的成员属性 dog.name = "Bob"; dog.age = 3; System.out.println("Pet Name: " + dog.name + " , Pet age: " + dog.age); Pet cat = new Pet("Ali", 2);//使用含参构造 System.out.println("Pet Name: " + cat.name + " , Pet age: " + cat.age); } }
package konoha.oop.objectLearn; //对象类 public class Pet { //两个公共属性 public String name; public int age; //构造函数没有返回值,也不写返回值类型 public Pet(){} //显示声明的空构造方法 //含参构造方法 public Pet(String name, int age) { this.name = name;//this 关键字指代自身 this.age = age; } }
七. 创建对象的内存分析
-
先看下面一张图
来自狂神说Java课程
当程序准备开始运行时,所需要的类和他们的静态方法,常量一起被加载到方法区(常量被加载到常量池中)。程序开始运行时,从main()方法进入,main()首先被压入栈底,然后新建对象。使用
new
关键字新建对象会调用类中的构造方法,然后程序根据构造方法在堆中新建了一块内存用于储存这个对象,然后将对象的引用值赋值给你的引用变量名,这个引用变量名储存在栈中。当你再次新建一个对象时,就会依照上述的步骤再次执行一次。需要注意的是,模板类虽然只有一个,他储存在方法区,但是他建立的对象却是不同的,在堆中各自占有各自的储存空间。类比生活中的例子就很容易理解,类可以看做抽象的一个学生概念,我们有一个学生花名册记录每一个学生的信息,每一个学生可以看做一个实际的学生对象,他们都是学生,但是每一个学生都各不相同,有自己的名字样貌等属性,自然需要各自记录在学生花名册中。一个类所新建的对象也是如此,他们虽然都是从一个类新建出来的,但是有自己不同的属性,自然需要不同的空间来分别储存。
八. 封装
-
封装(数据的隐藏):通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏。
-
设计程序该露的露,该藏的藏。我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。
-
总结为一句话就是:属性私有,get/set。解释起来就是将不需要暴露给外界的数据设置为私有成员(通过
private
关键字),对外界提供公共的get/set方法对这些数据进行允许范围内的操作。 -
方法也可以设置为私有。
-
封装的意义:
- 提高程序的安全性,保护数据。
- 隐藏了代码的实现细节。
- 统一接口,形成了规范。
- 提高了系统的可维护性,便于我们修改。
一个使用封装对象的实例:
package konoha.oop.objectLearn; //操作类 public class Control { public static void main(String[] args) { Pet cat = new Pet("Ali", 2);//使用含参构造 //System.out.println("Pet Name: " + cat.name + " , Pet age: " + cat.age); //无法直接使用属性,需要使用get方法取得 System.out.println("Pet Name: " + cat.getName() + " , Pet age: " + cat.getAge()); //使用set方法对属性进行修改 cat.setAge(5); System.out.println("Pet Name: " + cat.getName() + " , Pet age: " + cat.getAge()); } }
package konoha.oop.objectLearn; //对象类 public class Pet { //两个私有属性 private String name; private int age; //构造函数没有返回值,也不写返回值类型 public Pet(){} //显示声明的空构造方法 //含参构造方法 public Pet(String name, int age) { this.name = name;//this 关键字指代自身 this.age = age; } //get/set方法 public String getName() {return name;} public void setName(String name) {this.name = name;} public int getAge() {return age;} public void setAge(int age) { //对age进行合法性检查 if (age > 120 || age < 0) { System.out.println("Out of age bound, please enter" + " a number in 0~120!"); } else this.age = age; } }