基本概念
面向对象和面向过程之间的区别
它们两个之间的区别,举个非常简单的例子:
假设,你现在要开车回家。
面向过程:自己开车。
面向对象:找个代驾。
注:另外一种方式来说,面向过程第一位是想着如何处理数据,而面向对象第一个是想着如何让数据便于处理。
类和对象
类
类(class):构造对象的模板或蓝图。
例如,创建一个Person类(这个类使用了封装,封装的概念后续介绍):
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void setAge(int age) {
this.age = age;
}
public void work(){};
}
在类之间,最常见的关系有:
-
依赖(“uses-a”)
如果一类的方法操纵另一个类的对象,我们就说一个类依赖另一个类。(在实际的生活中同样,应该减少依赖关系)
例如:
// 你的出行依赖于汽车的移动 class Car{ public move(){}; } class Person{ public void walk(Car car){ car.move(); } }
-
聚合(“has-a”)
聚合关系意味着类A的对象包含类B的对象。
例如:
// 这个关系就像一个台式机的组装 class CPU{} class GPU{} class Machine{ private CPU cpu; private GPU gpu; public Machine(CPU cpu, GPU gpu){ this.cpu = cpu; this.gpu = GPU; } }
-
继承(“is-a”)
继承的关系,这个就比较好理解。例如,继承父业,“继承花呗”等。
例如:
// 继承 class A{} class B extends A{}
注:码代码时应要遵守,高内聚,低耦合,对外扩张,对内改闭的原则。
对象
创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。
例如:
new Date();
这就创建了一个Date类的对象。在这个例子中,构建对象仅使用了一次。通常,希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中,例如:
Person p = new Person();
如图所示:
分析一下这行代码的过程:
当程序执行这行代码时,如果这行代码时第一次使用Person类,则系统通常会在第一次使用Person类时加载这个类,并初始化这个类。
在类的准备阶段,系统将会为该类的类变量分配内存空间,并指定默认初始值。
当类初始化完后,系统将在堆内存中为Person类分配一块内存区(当Person类初始化完成后,系统会为Person类创建一个类对象)系统接着创建了一个Person对象,并把这个Person对象(对象的引用,指针)赋值给对象变量p,实例变量是在创建实例时分配内存空间并指定初始值的。
注:不管是数组还是对象,当程序访问引用变量的成员变量或方法时,实际上是访问该引用变量所引用的数组、对象的成员变量或方法。
例如:Person p2 = p;
上面这行代码是将p的引用赋值给p2,那么它们所引用的是同一个对象。
对象的三个主要特性:
- 对象的行为:可以对对象施加那些操作,或可以对对象施加那些方法?
- 对象的状态:当施加那些方法时,对象如何响应?
- 对象标识:如何辨别具有相同行为的不同对象?
this关键字
this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形。
- 构造其中引用该构造器正在初始化的对象。
- 在方法中引用调用该方法的对象。
this关键字最大的作用就是让一个类中一个方法,访问该类里的另一个方法或实例变量。
例如:
// 当Person p = new Person("test",18);时
/**
* 对this进行替换就是:p.name=name;p.age=age;
* 将传入进来的实参,赋值给变量本身
* 可见,谁调用的该方法,那么这个this指代的就是谁
*/
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 在封装器里面也是同样
public void setName(String name) {
this.name = name;
}
注:当然也可以不写,系统会自动隐式的使用this关键字。
方法
方法是类或对象的行为特征的抽象(本质就是函数)。但Java中的方法不能独立存在,它必须属于一个类或一个对象,因此方法也不能像函数那样被独立执行,执行方法时必须使用类或者对象来对其进行调用。语法格式是:“类.方法名|对象.方法名”。
假如:同一个类里面不同的方法之间相互调用,这样不就是可以直接调用了吗?
同一个类的一个方法调用另外一个类的方法时,如果被调用方法时普通方法(没有static关键字修饰的方法),则系统默认是this调用者;如果被调的方法是静态方法(准确来说是类方法),则系统默认使用类作为调用者。说白了,方法必须的寄生在类里面。
Java语言里方法的所属性主要体现在如下几个方面:
- 方法不能独立定义,方法只能在类里定义。
- 从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象。
- 永远不能独立执行方法,执行方法必须使用类或者对象调用。
注:使用static修饰的方法或者成员变量都是属于类本身(相关概念下面会详细介绍)。
参数传递
在C语言中,函数传递参数有两种,一种是值传递(不会改变本身状态),一种是指针传递(改变本身状态)。
先上结论,后去论证。Java的实参值传递只有一种:值传递。
举两个例子(例子中的Person类,是上面的Person类),基本数据类型,引用数据类型。
基本数据类型:
public class Test {
public static void main(String[] args){
int num1 = 10;
int num2 = 20;
System.out.println("转换前:");
System.out.println("num1 = "+num1+"\nnum2 = "+num2);
swap(num1, num2);
System.out.println("转换后:");
System.out.println("num1 = "+num1+"\nnum2 = "+num2);
}
public static void swap(int num1, int num2) {
int temp = num1;
num1 = num2;
num2 = temp;
}
}
运行结果:
其本质就是,实参的副本。
引用数据类型:
public class Test {
public static void main(String[] args){
Person p1 = new Person("p1",1);
Person p2 = new Person("p2",2);
System.out.println("p1:"+p1+" "+p1.hashCode());
System.out.println("p2:"+p2+" "+p2.hashCode());
System.out.println();
swap(p1, p2);
System.out.println();
System.out.println("p1:"+p1+" "+p1.hashCode());
System.out.println("p2:"+p2+" "+p2.hashCode());
}
public static void swap(Person p1 , Person p2) {
Person temp = p1;
p1 = p2;
p2 = temp;
System.out.println("p1:"+p1+" "+p1.hashCode());
System.out.println("p2:"+p2+" "+p2.hashCode());
System.out.println();
}
}
运行结果:
根据上面的运行结果可以看出是值传递,如果说是引用传递的话,其中p1和p2的引用它们会相互对换,然而它们的值并没有进行改变。
下面总结一下Java 程序设计语言中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
形参个数可变
方法指定数量不确定的形参。
语法格式是:最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当做成数组传入。
例如:
public void test(int sum, String... nums){
// 形参的本质,就是一个数组
for (String num : nums){
System.out.println(num);
}
System.out.println(sum);
}
那这种写法跟写一个String[] num
s之间有啥区别吗?
可变形参调用:
test(1,"num1,num2");
数组形式调用:
test(2,new String[]{"num1","num2"});
从上可知,可变形参还是比较方便的。
注:数组形式的形参可以处于形参列表的任意位置,但可变形参只能位于形参参数列表的最后位置。也就是说,一个方法中最多只能有一个可变形参。
方法重载
跟其他编程语言一样,如果,多个方法有相同的名字、不同的参数,便产生了重载。
例如:
public void test(int num1, int num2){};
public void test(String s1, String s2){};
那么编译器有什么怎么查找的呢?
编译器必须挑选出具体执行那个方法,它通过各个方法给出的参数类型与特定方法调用所使用的的值进行匹配挑选出相应的方法。如果编译器找不到匹配的参数,或者找出多个可能的匹配,就会产生编译时错误(这个过程被称为重载分析)。
注:构造器也是方法,所以也具有相同的特性。要完整地描述一个方法需要指出方法名以及参数类型。这叫做方法的签名。返回值类型不是方法签名的一部分,也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值得方法。
static
静态成员变量和静态方法
上面有讲到加上static修饰的成员变量或方法,就是属于类的,而不属于任何独立的对象。但每个独立的对象又共享这个成员变量或方法。
其中,它允许使用对象来调用static修饰的成员变量或方法,但实际上这还是不应该的。既然是属于类的,并不是属于类的实例,那么就不应该允许使用实例去调用属于类的成员变量或方法。所以,属于谁的就谁去调用,不能瞎搞。
注:术语“static”有一段不同寻常的历史。期初,C引入关键字static是为了表示退出一个块后依然存在的局部变量。在这中情况下,术语“static"是有意义的:变量一直存在看,当再次进入该块时依然存在。随后,static在C中有了第二种含义,表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字,关键字static被重用了。最后C++第三次重用了这个关键字,与前面赋予的含义完全不一样,这里将其解释为:属于类且不属于对象的变量和函数。这个含义与Java相同。
构造器
上面有讲过,一个对象的由来,最主要的途径是来源于构造器。构造器实质就是一个方法,所以它除了没有返回值之外,其他的都可以有。构造器的格式,如下:
public class Person{
// 空参构造器
public Person(){};
// 有参构造器
public Person(String name, int age){
this.name = name;
this.age = age;
}
// 构造器的重载
public Person(String name, int age, String sex){
this(name,age);
this.sex = sex;
}
}
使用this调用另一个重载构造器只能在构造器中使用,而且必须作为构造器执行执行体的第一条语句(super也是位于第一条语句)。
使用this调用重载的构造器时,系统会根据this后括号里面的实参来调用形参列表之间对应的构造器。
注:如果没有提供任何构造器,则系统会为这个类提供一个无参的构造器,这个构造器的执行体也为空。只要提供任何一个构造器,则系统就不会为这个类提供无参构造器,而且类中至少有一个构造器。
静态初始化块
从某种程度上面来说,初始化是构造器的补充,初始化块总是在构造器之前执行。系统同样可以使用初始化块来进行对象的初始化操作。下面主要演示的是静态初始化块(类初始化块)。
例如:
class A{
public A() {
System.out.println("A的无参构造器");
}
static {
System.out.println("A的静态初始化块");
}
{
System.out.println("A的普通初始化块");
}
}
class B extends A{
public B() {
System.out.println("B的无参构造器");
}
static {
System.out.println("B的静态初始化块");
}
{
System.out.println("B的普通初始化块");
}
}
public class Test {
public static void main(String[] args) {
new B();
}
}
运行结果:
从上面运行结果可以看出,类初始化阶段,先执行最顶层父类的静态初始化块,然后依次向下,直到执行当前类的静态初始化块。对象初始化阶段,先执行最顶层父类的初始化、最顶层父类的构造器,然后依次向下,直到执行当前类的初始化块、当前类的构造器(跟前面对象的产生一样)。还有,在类第一次加载的时候,将会进行静态域的初始化。与实例一样,除非将它们显示地设置成其他值,否则默认的初始化值是0、false或null。所有的静态初始化语句以及静态初始化都将按照类定义的顺序执行。
注:Java系统加载并初始化某个类时,总是保证该类的所有父类(包括直接父类和间接父类)全部加载并初始化。
面向对象的三大特征
封装
它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。上面的Person类就是一个封装的类。
封装可以实现以下目的:
- 隐藏类的实现细节。
- 让使用者只能通过实现预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。
- 可进行数据检查,从而有利于保证对象信息的完整性。
- 便于修改,提高代码的可维护性。
简单来说,把应该遮住的地方,遮住,该暴露的地方就暴露。
当然,当需要获得获设置成员变量的值时,应该提供下面三项内容:
- 一个私有的成员变量(数据域)。
- 一个公有的成员变量访问器方法(get方法)。
- 一个公有的成员变量更改器方法(set方法)。
注:封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。
继承
关键字extends表明正在构建的新类派生于一个已经存在的类。已存在的类称为超类(super Class)、基类(base class)或父类(parent class);新类称为子类(sub class)、派生类(derived class)或孩子类(child class)。从关键字上面来理解,子类是对父类的扩展,子类是一种特殊的父类。关键字的翻译,与其说是继承,个人觉得扩展这个更加恰当一些。
当子类继承父类时,子类会继承父类所有的方法和成员变量,对相关方法子类可以通过自己的需要对该方法进行覆盖(override),如果需要使用父类中的方法和成员变量时,这时需要使用super关键字。如果子类的构造器没有显示地调用超类的构造器,则将自动地调用超类默认(无参)的构造器。如果超类没有无参构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。
例如:
// 使用文章开头的Person类
public class Student extends Person{
private String sex;
public Student(String name, int age, String sex){
// 显式调用父类构造器
super(name,age);
this.sex = sex;
}
// 对父类的方法进行重写
public void work(){
System.out.println(super.getName()+"学生上课");
}
}
其中,super用于限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中一样,super也是具有该特性。如果在构造器中使用super,则super用于限定构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。如果子类定义了和父类同名的实例变量,则会发生子类的实例变量隐藏父类的实例变量的情况。上面在this那小节中,有提到过,当不显式的指出调用者时,系统默认的调用者就是this。所以,访问父类中的实例变量或者方法,需要指明调用者。
super与this用途,如下:
关键字this有两个用途:
- 引用隐式参数。
- 调用该类其他的构造器。
关键字super有两个用途:
- 调用超类的方法。
- 调用超类的构造器。
注:当重写父类中的方法时,权限不能低于父类的权限。如果低于父类中方法的权限,那么Java就会返回给你一个试图降低访问权限的异常。
小插曲,对前面创建对象的补充知识。
前面说过,构造器是创建对象的主要途径,来看下面这个例子:
class A{
public A(){
System.out.println("A的无参构造器");
}
}
class B extends A{
public B(){
System.out.println("B的无参构造器");
}
}
class C extends B{
public C(){
System.out.println("C的无参构造器");
}
}
public class Test {
public static void main(String[] args) {
new C();
}
}
看结果:
从上面运行结果来看,创建任何对象总是从该类所在继承树最顶层的类构造器开始执行,然后依次向下执行,最后才执行本类的构造器。重载的构造器,也是同样的道理。
继承设计的技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多的使用反射
多态
Java引用变量有两种类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,这就是多态(Polymorphism)。
例如:
class A{
public void work(){
System.out.println("A的工作方法");
}
}
class B extends A{
public void work(){
System.out.println("B的工作方法");
}
public void relax(){
System.out.println("B特有的休息方法");
}
}
public class Test {
public static void main(String[] args) {
A b = new B();
b.work(); // 输出:B的工作方法
// b.relax(); 编译时,这行代码会报错,需要写成((B) b).relax();
}
}
上面代码中,b,它的编译时类型是A,而运行时类型是B,当调用work方法时,实际执行的是B中所覆盖A中后的方法。
Java中允许把一个子类直接赋值给一个父类引用变量,无需任何的类型转换,或者被称为向上转型,这个过程有系统自动完成。当运行时调用该引用变量的方法时,其方法的行为总表现出子类的行为特征,而不是父类的行为特征。注释的哪一行,因为编译时的类型是A,A类中并没有该方法,必报错。如果需要调用B类中特有的方法,这时需要强制向下转型。
总结:
-
编译看父类,运行看子类。
-
父类对象变量转换成子类对象变量需要进行强制转换。
-
子类对象变量转换成父类对象变量不需要进行强制转换。
注:引用类型的强制转换只能在继承关系之间进行,如果是两个没有任何继承关系的类型,是没有办法进行类型转换,否则编译时必报错。
类设计技巧
- 一定要保证数据私有。
- 一定要对数据初始化(系统默认值可以满足需求,使用系统默认值即可)。
- 不要在类中使用过多的基本类型。
- 不是所有的域都需要独立的域访问器和域更改器。
- 将职责过多的类进行分解。
- 类名和方法名要能够体现它们的职责。
参考资料:
Java核心技术卷1基础知识
疯狂Java讲义
如上内容有误,还劳烦各位大佬指出,本人会及时改正。