面向对象是一种现在最为流行的程序设计方法,几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的,在70年代的Smaltalk语言之中进行了应用,后来根据面向对象的设计思路,才形成C++,而由C++产生了Java这门面向对象的编程语言。
但是在面向对象设计之前,广泛采用的是面向过程,面向过程只是针对于自己来解决问题。面向过程的操作是以程序的基本功能实现为主,实现之后就完成了,也不考虑修改的可能性,面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以,面向对象的开发更像是一个具备标准的开发模式。
在面向对象定义之中,也规定了一些基本的特征:
对于面向对象的开发来讲也分为三个过程:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。
类与对象时整个面向对象中最基础的组成单元。
类:是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法); 对象:对象是一种个性的表示,表示一个独立的个体,每个对象拥有自己独立的属性,依靠属性来区分不同对象。
可以一句话来总结出类和对象的区别:类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。
在Java中定义类,使用关键字class完成。语法如下:
class 类名{
类型 属性名; //属性
返回值类型 方法名(类型 参数); //行为
}
例如:
class Student{ //在Java中,对象名采用大驼峰命名法,
int id; //变量名采用小驼峰命名法
String name;
public void tell(){
System.out.println("学号:" + id + ",姓名:" + name);
}
}
在类定义完成后,如果想要使用就必须依靠对象,而对象是引用类型,以Student类为例,所以产生对象的格式如下:
Student student = new Student();
Student student = null;
student = new Student();
基本数据类型:不需要内存分配等
引用数据类型:需要内存的分配和使用。所以,通过关键字new分配内存空间
当一个实例化对象产生之后,可以按照如下的方式进行类的操作: 对象.属性:表示调用类之中的属性; 对象.方法():表示调用类之中的方法。
例如:
public class StudentTest {
public static void main(String args[]) {
Student stu = new Student() ;// 声明并实例化对象
stu.id = 1; //操作属性内容
stu.name = "张三" ; //操作属性内容
stu.tell(); //调用类中方法
}
}
/*
结果如下
--------------------------------------
学号:1,姓名:张三
--------------------------------------
*/
下面,我们采用另一种方法实例化对象
public class StudentTest {
public static void main(String args[]) {
Student stu = null; //声明对象
stu = new Student() ; //实例化对象
stu.id = 1; //操作属性内容
stu.name = "张三" ; //操作属性内容
stu.tell(); //调用类中方法
}
}
/*
结果如下
--------------------------------------
学号:1,姓名:张三
--------------------------------------
*/
以上两种方式看起来很接近,实际上差别很大:
从内存的角度分析如下。
任何情况下,关键字new都表示要分配新的堆内存空间;一旦堆内存空间分配,就会有类中定义的属性,并且属性内容都是其对应数据类型的默认值。
上面的两种对象实例化方式如下图:
两种方式的区别如上图所示,第二种方式实际上就是将第一种的两步结合在一起。
如果使用了未实例化的对象会有如下的情况:
public class StudentTest {
public static void main(String args[]) {
Student stu = null; //声明对象
// stu = new Student() ; //实例化对象
stu.id = 1; //操作属性内容
stu.name = "张三" ; //操作属性内容
stu.tell(); //调用类中方法
}
}
/*
结果如下
--------------------------------------
Exception in thread "main" java.lang.NullPointerException
at com.shimeath.test.StudentTest.main(StudentTest.java:7)
--------------------------------------
*/
此时程序只声明了Student对象,但是并没有实例化,这种错误在编译时并不会展现出来。只有当程序运行到这里时才会出现。这个错误为NullPointerException
带表空指针异常,这种类型的异常在任何应用数据类型均可能出现
引用传递的精髓:同一块堆内存空间,可以同时被多个栈内存所指向,不同的栈可以修改同一块堆内存的内容。
直接看例子:
public class StudentTest {
public static void main(String args[]) {
Student stu1 = new Student(); //声明并实例化对象
stu1.id = 1; //操作属性内容
stu1.name = "张三"; //操作属性内容
stu1.tell(); //调用类中方法,查看stu1中的属性值
Student stu2 = stu1; //传递引用
stu2.id = 2; //操作属性内容
stu2.name = "李四"; //操作属性内容
stu1.tell(); //调用类中方法,查看stu1中的属性值
stu2.tell(); //调用类中方法,查看stu2中的属性值
}
}
/*
结果如下
--------------------------------------
学号:1,姓名:张三
学号:2,姓名:李四
学号:2,姓名:李四
--------------------------------------
*/
内存分配图如下:
再来一个例子:
public class StudentTest {
public static void main(String args[]) {
Student stu1 = new Student(); //声明并实例化对象
Student stu2 = new Student(); //声明并实例化对象
stu1.id = 1; //操作属性内容
stu1.name = "张三"; //操作属性内容
stu2.id = 2; //操作属性内容
stu2.name = "李四"; //操作属性内容
stu1.tell(); //调用类中方法,查看stu1中的属性值
stu2.tell(); //调用类中方法,查看stu2中的属性值
stu2 = stu1; //引用传递
stu2.name = "王五"; //操作属性内容
stu1.tell(); //调用类中方法,查看stu1中的属性值
stu2.tell(); //调用类中方法,查看stu2中的属性值
}
}
/*
结果如下
--------------------------------------
学号:1,姓名:张三
学号:2,姓名:李四
学号:1,姓名:王五
学号:1,姓名:王五
--------------------------------------
*/
内存分配图如下:
封装(encapsulation)就是只公开对外的接口,而隐藏具体的实现。比如现在我们每天都在用的手机,手机的屏幕、扬声器、话筒就是对外的接口。我们在使用手机的时候只需要如何使用话筒等就可以使用手机,而并不需要了解手机内部的元器件是如何工作的。封装就像一部手机一样,只对外暴露接口,而不需要了解内部实现。
在研究封装之前我们先看一段代码:
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = new Computer(); //声明并实例化对象
computer1.brand = "Lenovo"; //操作属性内容
computer1.price = -4999.99; //操作属性内容
computer1.info(); //调用类中方法,查看computer1中的属性值
}
}
class Computer { //在Java中,对象名采用大驼峰命名法,
String brand;
double price; //变量名采用小驼峰命名法
public void info() {
System.out.println("品牌:" + brand + ",价格:" + price);
}
}
/*
结果如下
--------------------------------------
品牌:Lenovo,价格:-4999.99
--------------------------------------
*/
上面的代码看起来毫无问题,但是在却存在着一个严重的业务逻辑错误。众所周知,电脑的价格不可能为负数,造成这种情况的原因为:对象的属性可以在类的外部被直接访问,通常情况下,都不建议这样做。
解决方法则为将类的内部属性设置为对外不可见(只有本类中可以访问),采用private
关键字修饰
修改后如下:
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = new Computer(); //声明并实例化对象
computer1.brand = "Lenovo"; //操作属性内容
computer1.price = -4999.99; //操作属性内容
computer1.info(); //调用类中方法,查看computer1中的属性值
}
}
class Computer { //在Java中,对象名采用大驼峰命名法,
private String brand;
private double price; //变量名采用小驼峰命名法
public void info() {
System.out.println("品牌:" + brand + ",价格:" + price);
}
}
/*
结果如下
--------------------------------------
ComputerTest.java:4: 错误: brand 在 Computer 中是 private 访问控制
computer1.brand = "Lenovo"; //操作属性内容
^
ComputerTest.java:5: 错误: price 在 Computer 中是 private 访问控制
computer1.price = -4999.99; //操作属性内容
^
2 个错误
--------------------------------------
*/
我们发现,在访问属性的时候,外部的对象无法再直接调用类中的属性了,此时就相当于Computer类的属性对外部不可见。
但是,要想让程序可以正常运行,那么必须让外部可以操作Computer类的属性。在Java开发中,针对属性有这样的定义,在类中定义的属性都要求使用private声明,如果属性需要被外部所使用,那么按照要求,定义属性相应的setter和getter方法,以Computer类中的String brand为例:
public void setBrand(String brand)
public void getBrand()
选用:通过Lombok简化消除一些必须有但显得很臃肿的Java代码
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = new Computer(); //声明并实例化对象
computer1.setBrand("Lenovo"); //操作属性内容
computer1.setPrice(-4999.99); //操作属性内容
computer1.info(); //调用类中方法,查看computer1中的属性值
}
}
@Getter
@Setter
class Computer { //在Java中,对象名采用大驼峰命名法,
private String brand;
private double price; //变量名采用小驼峰命名法
public void info() {
System.out.println("品牌:" + brand + ",价格:" + price);
}
}
/*
结果如下
--------------------------------------
品牌:Lenovo,价格:-4999.99
--------------------------------------
*/
发现,图书的价格是负数,需要加入检查业务逻辑错误的代码,可以在setter中增加验证,如果值为正,赋值,否则为默认值0.0:
public void setPrice(double price) {
if(price > 0.0){
this.price = price;
}
}
对于数据验证,在Java标准开发中应该由辅助代码完成。在实际开发中,setter往往只是简单的设置属性内容,getter只是简单的取得属性内容。
开发建议:以后在定义类的时候,所有的属性都要编写private封装,封装之后的属性如果需要被外部操作,则编写setter、getter。
先来看对象的产生格式:
①类名称 ②对象名称 = ③new ④类名称();
① 类名称:规定了对象的类型。; ② 对象名称:如果需要使用对象,需要有一个名称,这是一个唯一的标记; ③ new:分配新的堆内存空间; ④ 类名称():调用了名称和类名称相同的方法,这就是构造方法。
实际上,构造方法一直在被我们调用,但我们并没有去定义它,为什么能够使用呢?这是因为在整个Java开发中,为了保证程序可以正常执行,即便用户没有定义任何构造方法,也会在程序编译后自动为类增加一个没有参数,方法名称与类名称相同,没有返回值的构造方法。
构造方法的定义:方法名称和类名称相同,没有返回值声明。
//构造方法:无参、无返回值,未显式定义时默认生成无参构造方法
public Computer() {}
如果在Computer类中没有定义以上的构造方法,那么也会自动生成一个无参,无返回值的构造方法。
我们再看:
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = null; //声明对象
}
}
class Computer {
public Computer() {
System.out.println("无参构造方法");
}
}
/*
结果如下
--------------------------------------
--------------------------------------
*/
运行后,什么也没有打印。 在主方法中加入实例化对象的代码:
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = null; //声明对象
computer1 = new Computer(); //实例化对象
}
}
/*
结果如下
--------------------------------------
无参构造方法
--------------------------------------
*/
以上说明,构造方法是在对象在被实例化时才会调用。
构造方法与普通方法最大的区别是: 构造方法在实例化对象(new)的时候只调用一次,而普通方法是在实例化对象之后可以随意调用多次。
在实际开发中,构造方法的作用是在类对象实例化的时候设置属性的初始化内容,范例如下:
public class ComputerTest {
public static void main(String args[]) {
Computer computer1 = new Computer("Lenovo", -4999.99); //声明并实例化对象
computer1.info(); //调用类中方法,查看computer1中的属性值
}
}
@Getter
@Setter
@AllArgsConstructor
class Computer {
private String brand;
private double price;
public void info() {
System.out.println("品牌:" + brand + ",价格:" + price);
}
}
/*
结果如下
--------------------------------------
品牌:Lenovo,价格:-4999.99
--------------------------------------
*/
如果一个类中已经明确定义了一个构造方法,则无参构造方法将不会自动生成。而且,一个类之中至少存在一个构造方法。另外,既然构造方法也属于方法,那么构造方法也可以重载,但是由于构造方法的特殊性,所以在构造方法重载时注意其参数的类型及参数的个数即可。
在进行构造方法重载时有一个编写建议:所有重载的构造方法按照参数的个数由多到少,或者是由少到多排列。
匿名对象:没有名字的对象
对象的名字按照之前的内存关系来讲,在栈内存之中,而对象的具体内容在堆内存之中保存,这样,没有栈内存指向堆内存空间,就是一个匿名对象。
public class TestDemo {
public static void main(String args[]) {
new Computer("Lenovo", -4999.99).info(); //调用类中方法,查看computer1中的属性值
}
}
@Getter
@Setter
@AllArgsConstructor
class Computer {
private String brand;
private double price;
public void info() {
System.out.println("品牌:" + brand + ",价格:" + price);
}
}
/*
结果如下
--------------------------------------
品牌:Lenovo,价格:-4999.99
--------------------------------------
*/
匿名对象由于没有对应的栈内存指向,所以只能使用一次,一次之后就将成为垃圾,并且等待被GC回收释放。
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。