首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础知识(二)

Java基础知识(二)

作者头像
shimeath
发布2020-07-30 17:05:37
4390
发布2020-07-30 17:05:37
举报
四、 Java类和对象
1.面向对象简述

​ 面向对象是一种现在最为流行的程序设计方法,几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的,在70年代的Smaltalk语言之中进行了应用,后来根据面向对象的设计思路,才形成C++,而由C++产生了Java这门面向对象的编程语言。

​ 但是在面向对象设计之前,广泛采用的是面向过程,面向过程只是针对于自己来解决问题。面向过程的操作是以程序的基本功能实现为主,实现之后就完成了,也不考虑修改的可能性,面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以,面向对象的开发更像是一个具备标准的开发模式。

在面向对象定义之中,也规定了一些基本的特征:

  1. 封装:保护内部的操作不被破坏;
  2. 继承:在原本的基础之上继续进行扩充;
  3. 多态:在一个指定的范围之内进行概念的转换。

对于面向对象的开发来讲也分为三个过程:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

2.类与对象的基本概念

类与对象时整个面向对象中最基础的组成单元。

类:是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法); 对象:对象是一种个性的表示,表示一个独立的个体,每个对象拥有自己独立的属性,依靠属性来区分不同对象。

可以一句话来总结出类和对象的区别:类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。

3.类与对象的定义和使用

在Java中定义类,使用关键字class完成。语法如下:

class 类名{
	类型 属性名;	//属性
    返回值类型 方法名(类型 参数); //行为
}

例如:

class Student{	//在Java中,对象名采用大驼峰命名法,
    int id;		//变量名采用小驼峰命名法
    String name;
    public void tell(){
        System.out.println("学号:" + id + ",姓名:" + name);
    }
}

在类定义完成后,如果想要使用就必须依靠对象,而对象是引用类型,以Student类为例,所以产生对象的格式如下:

  1. 声明并实例化
Student student = new Student();
  1. 先声明后实例化
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,姓名:张三
--------------------------------------
*/

以上两种方式看起来很接近,实际上差别很大:

从内存的角度分析如下。

  1. 堆内存:保存对象的属性内容。堆内存通过new关键字分配空间;
  2. 栈内存:保存堆内存的地址

任何情况下,关键字new都表示要分配新的堆内存空间;一旦堆内存空间分配,就会有类中定义的属性,并且属性内容都是其对应数据类型的默认值。

上面的两种对象实例化方式如下图:

  1. 第一种:
  1. 第二种:

两种方式的区别如上图所示,第二种方式实际上就是将第一种的两步结合在一起。

如果使用了未实例化的对象会有如下的情况:

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带表空指针异常,这种类型的异常在任何应用数据类型均可能出现

4.对象引用传递分析

引用传递的精髓:同一块堆内存空间,可以同时被多个栈内存所指向,不同的栈可以修改同一块堆内存的内容。

直接看例子:

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,姓名:王五
--------------------------------------
*/

内存分配图如下:

5.面向对象的封装性

封装(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为例:

  1. setter方法是设置属性内容(有参数)
public void setBrand(String brand)
  1. getter方法是获得属性内容(无参数)
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。

6.类中的构造方法

先来看对象的产生格式:

①类名称 ②对象名称 = ③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
--------------------------------------
*/

如果一个类中已经明确定义了一个构造方法,则无参构造方法将不会自动生成。而且,一个类之中至少存在一个构造方法。另外,既然构造方法也属于方法,那么构造方法也可以重载,但是由于构造方法的特殊性,所以在构造方法重载时注意其参数的类型及参数的个数即可。

在进行构造方法重载时有一个编写建议:所有重载的构造方法按照参数的个数由多到少,或者是由少到多排列。

7.类中的匿名对象

匿名对象:没有名字的对象

对象的名字按照之前的内存关系来讲,在栈内存之中,而对象的具体内容在堆内存之中保存,这样,没有栈内存指向堆内存空间,就是一个匿名对象。

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. 语法层面上的区别

  1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;

  2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;

  3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。

  1. 设计层面上的区别

  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进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-07-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 四、 Java类和对象
    • 1.面向对象简述
      • 2.类与对象的基本概念
        • 3.类与对象的定义和使用
          • 4.对象引用传递分析
            • 5.面向对象的封装性
              • 6.类中的构造方法
                • 7.类中的匿名对象
                • 五、抽象类和接口的区别
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档