面向对象对比面向过程
面向对象与面向过程都是一种编程思想
- 面向过程编程(Procedure Oriented Programming)是一种以过程为中心的编程思想,关心的是实现的步骤。
java-core-jdk-foundational模块的代码都是基于面向过程实现的。 - 面向对象编程(Object Oriented Programming)是一种以对象为中心的编程思想。客观存在的任何一种现实事物都可以看做程序中的对象,每个对象都有自己独特的功能,在Java语言中万物皆对象。面向对象编程是基于面向过程演变而来的,将我们从执行者的位置变成了指挥者。使用面向对象的思想可以将复杂的问题简单化,相对于面向过程而言面向对象编程更容易扩展和维护,适合大型项目复杂业务场景开发。
深入理解Java中的类
类和对象的关系
类(Class)是对生活中一类具有共同属性和行为的事物的抽象,类是对事物,也就是对象的一种描述。
可以将类理解为设计图,根据设计图可以创建出具体存在的对象。例如在制造iphone之前苹果设计师都会出iphone的设计图,然后富士康工厂才能根据设计图制造iphone。
因此对象是根据类创建出来的真实存在的实体。
类由属性和行为组成
属性指的是该事物的各种特征,例如iphone的颜色,价格,重量,尺寸等等。
行为就是该事物存在的功能,例如iphone能够播放音乐,录视频,上网,拍照等等。
类的定义
和使用变量之前先定义一样,在使用类之前需要定义类,类由属性和行为组成
- 属性在程序中通过成员变量(Member Variable)体现的,也就是在类中方法外定义的变量,成员变量从属于对象成员变量是在堆内存中存储,从属于对象,因此其生命周期是随着对象的存在而存在的,随着对象的消失而消失。成员变量由Java负责自动初始化。,不同于之前都是在方法中定义的都是局部变量(Local Variable)。局部变量是定义在方法中的变量或者是方法的形参,局部变量在栈内存中存储,从属于方法,因此其生命周期是随着方法的存在而存在,随着方法的调用完成而消失。局部变量Java程序员负责手动初始化后才能使用。
- 行为在程序中就是成员方法(Member Method)体现的,之前定义的方法加了static关键字,属于静态方法,静态方法从属于类,而成员方法就是去掉static关键字之后的方法,成员方法从属于对象。
自定义类案例:Product类
package net.ittimeline.java.core.jdk.oop.bean;/** * 自定义类案例:Product类 * @author tony 18601767221@163.com * @version 2020/12/12 17:21 * @since JDK11 */public class Product { /********************定义成员变量********************/ /** * 商品名称 */ String name; /** * 商品颜色 */ String color; /** * 商品尺寸 */ double size; /** * 商品重量 */ int weight; /** * 商品价格 */ double price; /********************定义成员方法********************/ /** * 播放音乐 */ public void playMusic(){ System.out.println("产品正在播放音乐"); } /** * 指定的产品名称播放音乐 * @param productName */ public void playMusic(String productName){ System.out.println(productName+"正在播放音乐"); }}
需要注意的是成员变量定义时通常不会手动指定初始化值,而是在创建对象时赋值。
成员方法和普通方法一样也可以重载,例如这里的playMusic()就发生了重载。
构造方法
构造方法就是创建对象时所调用的方法,当使用new关键字创建对象时就会调用构造方法。构造方法的方法名和类名一致,构造方法没有返回值,连void都没有,也不能使用return 返回数据。
虽然我们之前在创建对象时没有定义构造方法,但是系统默认会提供一个无参数的构造方法。
public Product(){ }
如果我们在类中定义了无参数的构造方法,则会覆盖系统默认提供的构造方法
定义无参数的构造方法
/** * Product对象无参构造器 */ public Product(){ System.out.println("执行Product对象的构造方法"); }
每次创建对象的时候都会去调用一次构造方法,构造方法不能手动调用。
package net.ittimeline.java.core.jdk.oop.bean.constructor;/** * 构造方法测试 * * @author tony 18601767221@163.com * @version 2020/12/13 12:04 * @since JDK11 */public class ConstructorTest { public static void main(String[] args) { //创建三个Product对象 //每次创建对象时都会调用对象的构造方法,此时会调用无参数的构造器 Product product1=new Product(); Product product2=new Product(); Product product3=new Product(); }}
程序运行结果
构造方法的作用就是用于初始化对象,即给对象的成员变量赋值,但是如果是在无参数的构造器给成员变量赋值不够灵活也不合理,因为每个对象的属性值可能不一样。
此时就可以使用带参数的构造器来给成员变量赋值。通过传递参数,让构造器给属性赋值时更加灵活。
有参构造方法
/** * Product对象 有参数构造器 * @param name * @param price */ public Product(String name,double price){ this.name=name; this.price=price; }
有参数构造方法的调用
package net.ittimeline.java.core.jdk.oop.bean.constructor;/** * 构造方法测试 * * @author tony 18601767221@163.com * @version 2020/12/13 12:04 * @since JDK11 */public class ConstructorTest { public static void main(String[] args) { //通过构造方法给属性赋值 Product iphone12=new Product("iphone12",6799); Product mate40ProPlus=new Product("mate40ProPlus",8999); iphone12.showProductInfo(); mate40ProPlus.showProductInfo(); }}
程序运行结果
在定义和使用构造方法时需要注意如果类中没有定义构造方法,系统会提供一个不带参数的构造方法。如果类中定义了带参数的构造方法,则系统会再提供默认的无参构造方法,此时如果还想调用无参构造方法,需要自己手动实现一个无参构造方法。此时无参构造方法和有参构造方法就形成了构造方法的重载。
/** * Product对象无参构造器 */ public Product(){ System.out.println("执行Product对象的构造方法"); } /** * Product对象 有参数构造器 * @param name * @param price */ public Product(String name,double price){ this.name=name; this.price=price; }
代码块
代码块就是使用一对{ }包含的一段代码,代码块可以分为构造代码块,静态代码块和局部代码块
- 构造代码块就是在类中,方法外定义的代码块,代码块的定义格式就是{ },构造代码块在每次调用构造器创建对象前会执行一次,其使用场景是创建对象之前需要执行的代码可以方法在代码块中,也可以用用来统计创建对象的个数
- 静态代码块就是在类中,方法外定义的代码块,静态代码块的格式就是static {},静态代码块会在类加载时执行,而且只会执行一次。其应用场景是只需要执行一次的代码 ,静态代码块优先于构造代码块执行,构造代码块优先于构造方法。
- 局部代码块就是在方法内部定义的代码块,局部代码块的格式就是{},局部代码块在调用方法,执行到了局部代码块才会被执行。其应用场景是提前释放内存,节省内存空间。因为局部代码块中如果使用了基本数据类型的局部变量,该局部变量的作用域在局部代码块{}内,超过局部代码块{}作用域就会释放。但是服务器内存都是16G起步,因此局部代码块意义不大。
package net.ittimeline.java.core.jdk.oop.bean.codeblock;/** * 代码块测试 * * @author tony 18601767221@163.com * @version 2020/12/14 16:37 * @since JDK11 */public class CodeBlockTest { public static void main(String[] args) { UserInfo userInfo =new UserInfo(); { int age = 10; System.out.println("我是一个局部代码块"); System.out.println("age = " + age); } }}class UserInfo { public static String defaultRegisterResource; static { System.out.println("我是一个静态代码块"); defaultRegisterResource = "APP"; } private String userName; private String password; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } // 构造代码块 { System.out.println("我是一个构造代码块"); if (this.userName==null|| this.userName.equals("")) { this.setUserName("admin"); } if (this.password==null || this.password.equals("")) { this.setPassword("123456"); } } public UserInfo() { System.out.println("我是一个无参构造器"); System.out.printf("我的名字是%s,我的密码是%s",this.getUserName(),this.getPassword()); }}
程序运行结果
package和import 关键字
package关键字用于定义一个包,包是Java用来组织程序结构的一种方式,不同的代码可以放在不同的包结构中。
在日常开发中,包名的命名通常都是使用公司的域名倒过来然后加上项目名,业务模块名。例如com.taobao.user。包名加类名组成全类名,JVM在加载类时会使用到全类名
包声明语句必须放在Java源文件的第一行
包的本质就是文件夹
import关键字用于导入一个类,日常开发中通常会使用到大量的JDK提供的类库或者是第三方提供的类库
例如之前使用Scanner类就是位于java.util包下,需要使用import关键字加上全类名导入。
深入理解Java中的对象
对象的创建和使用
类定义后就可以通过创建对象来使用,创建对象的语法格式是 类名 对象名 = new 类名();,例如Product iphone12 =new Product();,
创建对象后就可以使用对象的成员变量和成员方法。
使用成员变量的语法格式是 对象名.成员变量名,例如iphone12.color;
使用成员方法的语法格式是对象名.方法名,例如iphone12.playMusic();
在使用类之前先说明下java-core-jdk-oop模块的目录结构,之前在java-core-jdk-foundational中基本上都是使用一个包含main方法的类来演示说明某个语言特性,例如循环等等。
而在学习面向对象之后,通常都会编写多个类组合后演示某个语言特性,例如面向对象的三大特性:封装、继承、多态,通常都会编写一个测试类(例如这里的ProductTest)来测试某些语言特性,按照maven的项目结构约定,该测试类通常位于src/main/test目录下。而src/main/resource用于存放项目的源代码。
package net.ittimeline.java.core.jdk.oop.bean;/** * 测试对象的创建和使用 * * @author tony 18601767221@163.com * @version 2020/12/12 17:24 * @since JDK11 */public class ProductTest { public static void main(String[] args) { // 创建对象 Product iphone12 = new Product(); //打印对象的内存地址 // iphone12 = net.ittimeline.java.core.jdk.oop.bean.Product@566776ad // net.ittimeline.java.core.jdk.oop.clazz.Product表示全类名,即包名+类名 // @表示分隔符 // 566776ad 表示十六进制的地址 System.out.println("iphone12 = "+iphone12); System.out.println("对象的成员变量未赋值时"); //访问对象的成员变量 //当创建对象后,Java会给成员变量赋初始值 // 例如成员变量是int,初始值就是0,成员变量类型是double,初始值就是0.0,成员变量是String,初始值是null System.out.println("iphone12.name = " + iphone12.name); System.out.println("iphone12.color = " + iphone12.color); System.out.println("iphone12.size = " + iphone12.size); System.out.println("iphone12.weight = " + iphone12.weight); System.out.println("iphone12.price = " + iphone12.price); //给对对象的成员变量赋值 iphone12.color="黑色"; iphone12.name="iphone12"; iphone12.size=6.1; iphone12.weight=162; iphone12.price=6799; System.out.println("对象的成员变量赋值时"); System.out.println("iphone12.name = " + iphone12.name); System.out.println("iphone12.color = " + iphone12.color); System.out.println("iphone12.size = " + iphone12.size); System.out.println("iphone12.weight = " + iphone12.weight); System.out.println("iphone12.price = " + iphone12.price); // 调用对象的重载方法 iphone12.playMusic(); iphone12.playMusic(iphone12.name); }}
程序运行结果
当创建对象时,系统会根据对象成员变量的数据类型赋初始值。
对象的内存结构
单个对象的内存结构
ProductTest程序运行时的内存结构
- 程序运行时main方法所在的类ProductTest率先被加载到方法区,该类只有一个main方法
- 当main方法被JVM调用时main方法进栈
- 当执行到Product iphone12 = new Product();,Product.class也会被加载到方法区中,方法区中会包含该类的5个成员变量和两个重载的方法信息,使用new Product();创建对象时会在堆内存中开辟内存空间用来存储Product对象的成员变量和成员方法的地址,并且会给堆内存中的成员变量赋默认的初始化值。然后将堆内存的空间地址566776ad返回给栈内存的Product引用变量iphone12,当打印对象的地址时就会输出 net.ittimeline.java.core.jdk.oop.bean.Product@566776ad,也就是全类名@地址值。
- 当创建对象后打印输出成员变量值时 会打印出Java给成员变量的初始值
- 当执行到iphone12.color="黑色";,此时会通过iphone12的地址找到堆内存的对象,并将该对象的color属性由默认的初始值null改成字符串常量值黑色
- 当执行打印属性值时,通过属性名找到堆内存的属性值打印输出
- 当执行调用成员方法时,会根据栈内存的对象引用找到堆内存方法的地址,然后根据该地址找到方法区的方法,将该方法入栈后执行,当方法结束后会依次出栈。
多个对象的内存结构
package net.ittimeline.java.core.jdk.oop.bean;/** * 多个对象的创建和使用 * * @author tony 18601767221@163.com * @version 2020/12/13 9:25 * @since JDK11 */public class ProductMultiTest { public static void main(String[] args) { Product mat40proPlus = new Product(); mat40proPlus.color="黑色"; mat40proPlus.name="mate40ProPlus"; mat40proPlus.size=6.76; mat40proPlus.weight=230; mat40proPlus.price=8999; System.out.println("对象的成员变量赋值时"); System.out.println("mat40proPlus = "+mat40proPlus); System.out.println("mat40proPlus.name = " + mat40proPlus.name); System.out.println("mat40proPlus.color = " + mat40proPlus.color); System.out.println("mat40proPlus.size = " + mat40proPlus.size); System.out.println("mat40proPlus.weight = " + mat40proPlus.weight); System.out.println("mat40proPlus.price = " + mat40proPlus.price); mat40proPlus.playMusic(mat40proPlus.name); Product iphone12 = new Product(); iphone12.color="黑色"; iphone12.name="iphone12"; iphone12.size=6.1; iphone12.weight=162; iphone12.price=6799; System.out.println("对象的成员变量赋值时"); System.out.println("iphone12 = "+iphone12); System.out.println("iphone12.name = " + iphone12.name); System.out.println("iphone12.color = " + iphone12.color); System.out.println("iphone12.size = " + iphone12.size); System.out.println("iphone12.weight = " + iphone12.weight); System.out.println("iphone12.price = " + iphone12.price); iphone12.playMusic(iphone12.name); }}
程序运行结果
程序运行时内存结构如下
当创建两个对象时,每个对象在堆内存中都有自己独立的内存空间,但是共享同一个Product.class字节码,也就是说无论创建多少个对象,Prdduct.class只会加载一次。
而当两个对象的引用指向同一个对象时,此时实际上是栈内存的两个引用指向了堆内存中的同一个对象,当使用其中一个引用变量(例如这里的iphone12Copy)修改对象的成员变量值时,另外一个引用变量获取对象的成员变量时拿到的也是修改之后的值。当对象置为null,此时两个对象的引用不再指向堆内存的对象,堆内存的对象占据的空间会被JVM的垃圾回收器在空闲的时回收。Java程序员不需要像C程序员那样手动开辟堆内存和释放堆内存。JVM的垃圾回收机制会帮我们清理堆内存中的垃圾对象,即没有引用指向的对象。如果在程序中使用没有引用指向的对象会引发程序的空指针(NullPointerException)
package net.ittimeline.java.core.jdk.oop.bean;/** * 两个对象引用指向同一个对象 * * @author tony 18601767221@163.com * @version 2020/12/13 9:54 * @since JDK11 */public class ProductAssignmentTest { public static void main(String[] args) { Product iphone12 = new Product(); iphone12.color="黑色"; iphone12.name="iphone12"; iphone12.size=6.1; iphone12.weight=162; iphone12.price=6799; System.out.println("对象的成员变量赋值时"); System.out.println("iphone12 = "+iphone12); System.out.println("iphone12.name = " + iphone12.name); System.out.println("iphone12.color = " + iphone12.color); System.out.println("iphone12.size = " + iphone12.size); System.out.println("iphone12.weight = " + iphone12.weight); System.out.println("iphone12.price = " + iphone12.price); Product iphone12Copy=iphone12; System.out.println("iphone12Copy = "+iphone12Copy); System.out.println("iphone12Copy.name = " + iphone12Copy.name); System.out.println("iphone12Copy.color = " + iphone12Copy.color); System.out.println("iphone12Copy.size = " + iphone12Copy.size); System.out.println("iphone12Copy.weight = " + iphone12Copy.weight); System.out.println("iphone12Copy.price = " + iphone12Copy.price); iphone12Copy.price=6699; System.out.println("iphone12Copy.size = " + iphone12Copy.size); System.out.println("iphone12.size = " + iphone12.size); //当对象置为null,此时两个对象的引用不再指向堆内存的对象,堆内存的对象占据的空间会被JVM的垃圾回收器在空闲的时回收 iphone12Copy=null; iphone12=null; //此时iphone12已经是null,如果继续使用iphone12访问对象的属性和方法,会引发空指针异常 System.out.println(iphone12); System.out.println(iphone12.color); }}
程序运行结果
程序运行时内存结构如下
this关键字
当局部变量和成员变量重名时,Java采用就近原则,即会使用局部变量。
this关键字用于调用本类的成员变量和成员方法,解决局部变量和成员变量的重命名问题。this.成员变量 表示当前对象的成员变量this.成员方法表示当前对象的成员方法
this 代表所在类的对象引用地址,方法被哪个对象调用,this就代表哪个对象。
public void setName(String name) { //this表示所在类的对象的引用地址,方法被哪个对象引用,this代表哪个对象 System.out.println("Product Object setName() this = "+this); //this关键字区分局部变量和成员变量 this.name = name; }
package net.ittimeline.java.core.jdk.oop.encapsulation;/** * this访问成员变量测试 * * @author tony 18601767221@163.com * @version 2020/12/13 11:26 * @since JDK11 */public class ThisTest { public static void main(String[] args) { //创建两个产品对象 Product note20Ultra = new Product(); //调用note20Ultra对象的setName()方法 note20Ultra.setName("三星Note20Ultra"); System.out.println("note20Ultra = "+note20Ultra); System.out.println("******楚河******汉界******"); Product mat40proPlus = new Product(); //调用mat40proPlus对象的setName()方法 mat40proPlus.setName("华为mat40proPlus"); System.out.println("mat40proPlus = "+mat40proPlus); }}
程序运行结果
当创建note20Ultra的Product对象时调用setName()方法,this关键字表示note20Utra的引用地址。
当创建mate40ProPlus的Product对象时调用setName()方法,this关键字表示mate40ProPlus的引用地址。
匿名对象
匿名对象就是没有名字的对象,之前使用new关键字创建的对象都是有名字,每次使用new关键字创建对象时都会在堆内存中开辟新的内存空间存储对象。
匿名对象只能使用一次,通常当某个方法需要传入对象作为参数时可以传入匿名对象。
package net.ittimeline.java.core.jdk.oop.bean;import net.ittimeline.java.core.jdk.oop.encapsulation.Product;/** * 匿名对象 * * @author tony 18601767221@163.com * @version 2020/12/13 14:28 * @since JDK11 */public class AnonymousObjectTest { public static void main(String[] args) { Product iphone12=new Product(); iphone12.setName("iphone12"); iphone12.setPrice(6799.00); //每次使用new关键字都会创造一个新的对象 //匿名对象,使用new关键字创建对象,但是没有名字,而且只能使用一次 new Product(); //它的应用场景是当调用方法时,需要传入某个类的对象,可以传入该类的匿名对象 show(new Product()); } public static void show(Product product){ product.showProductInfo(); }}
程序运行结果
面向对象之封装
访问权限修饰符
Java项目采用包来组织代码,项目的权限分为类权限、同包权限、不同包的子类权限、项目权限。
Java提供了四个权限修饰符,按照权限从大到小依次是public>protected>默认>private。
通常private用于修饰成员变量和成员方法,被private修饰的成员变量和方法只能在本类当中访问,也就是类权限。那么也就意味着不能在其他类中使用对象名.成员变量名和对象名.成员方法的方式来使用对象的成员变量和方法了。此时要访问对象的成员变量之前就需要提供成员变量的get()和set()方法。get()方法用于获取成员变量的变量值,方法名加get前缀,然后跟上成员变量名。set()方法用于给成员变量赋值,方法名加set前缀,然后跟上成员变量名,该方法需要传递一个和成员变量类型一样的型参。
package net.ittimeline.java.core.jdk.oop.encapsulation;/** * 自定义类案例:Product类 * @author tony 18601767221@163.com * @version 2020/12/12 17:21 * @since JDK11 */public class Product { /********************定义私有化成员变量********************/ /** * 商品名称 */ private String name; /** * 商品价格 */ private double price; /********************私有化成员变量的公共get/set方法********************/ public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { //成员变量的合法性校验 if(price<0.0){ System.out.println("价格非法"); }else{ this.price = price; } } public void showProductInfo(){ System.out.printf("产品的名称是%s,产品的价格是%.2f",name,price); }}
private关键字修饰成员变量测试
package net.ittimeline.java.core.jdk.oop.encapsulation;/** * private关键字修饰成员变量测试 * * @author tony 18601767221@163.com * @version 2020/12/13 11:09 * @since JDK11 */public class PrivateTest { public static void main(String[] args) { // 创建Product对象 Product product = new Product(); // 当给对象的成员变量赋一个错误的值是无法赋值成功 // product.setPrice(-4999.00); product.setPrice(4999.00); // 私有成员变量只能在当前类中使用 // product.name; product.setName("小米10pro尊享版"); product.showProductInfo(); }}
程序运行结果
默认(空)修饰符是不同类,但是在同一个包中可以访问
protected修饰符是不同包的子类访问权限
在日常开发中类通常是定义public,成员变量使用private,定义方法使用public和private,如果是复杂业务,可以抽取多个私有方法。然后定义一个public方法给外部调用,public方法内部调用抽取的private方法。
父类中的方法如果只想给不同包的子类使用,而不对公共访问,可以使用protected权限修饰符。
public修饰符是项目的权限,可以在任意的地方访问。
封装
封装(Encapsulation)是面向对象的三大特性(封装、继承、多态)之一,封装就是隐藏实现细节,仅对外暴露公共的访问方式。
- 将成员变量抽取到类中,是对数据的一种封装
- 将代码重构抽取方法,是对代码的一种封装
- 在定义类时私有化成员变量,提供公共的get/set的访问方法就是封装的一种体现。
通过封装方法提高了代码的复用性,通过私有化属性,提供公共的get/set方法提高了代码的安全性