跳到主要内容

第8章-面向对象基础

面向过程

  • 面向过程( Procedure Oriented)的思想,主要是起源在软件发展的早期
  • 面向过程的主要通过一定的步骤或过程,来完成某种任务或业务需求;这些业务需求的特点是:逻辑简单、关系清晰
  • 生活场景
    • 每天早起步骤:起床、穿衣、刷牙、洗脸、吃饭...
    • 开车的步骤:开车门、调整座椅、系安全带、踩离合、启动、挂档、加油...
    • ...
  • 面向过程的关注或切入点是按步骤或过程
  • 面向过程程序的特点:代码量小、简单易懂、执行效率高,但不能面对复杂业务、复用程度低、安全性低、可扩展能力差
  • 典型的面向过程的编程语言有C、Fortran、Basic、Pascal 等

面向对象

  • 面向对象(Object Oriented)的思想,则是在软件慢慢发展中诞生的
  • 面向对象,从现实世界出发,对实际的事物进行抽象,针对抽象,再描述其特征与行为,最后组织各种对象的特征和行为,完成业务需求,更贴近实际的生活、更灵活、复用度也更高
  • 生活场景
    • 每天早起的事物是某个人,这个人具有姓名、年龄这种天然的特征,并具备起床、穿衣、刷牙、洗脸、吃饭...等行为;其他人也具备这些特征与行为,这是一个典型实际事物的抽象;然后,通过抽象的人的特征与行为组合,完成早起功能;更符合现实生活中的理解
    • 开车的过程,参与的事物有某人和某车,人具有姓名、年龄、是否有驾照、驾龄等特征;车则具有品牌、发动机型号、门、方向盘、安全带等特征;同时人具备打开、调整、系带、踩、操作按钮等行为;车则具备门打开、方向盘转动、安全带扣上、安全带松开等行为;通过组合人和车的行为,完成开车的过程
    • ...
  • 面向对象的关注或切入点是实际的事物,并进行抽象,且聚焦于抽象的特征与行为
  • 面向对象程序的特点:代码量相对更多、逻辑相对复杂、执行效率相对要差,但能面对更复杂的业务、程序逻辑更符合现实、复用程度高、代码安全性更高、可扩展能力更好、更容易被重构
  • 典型的面向对象的编程语言有:Java、C#、Python等

面向对象主要概念

  • :是对具有共同特征和行为的实际事物的抽象,一个模板,并非实际存在;如将实实在在的张三、李四等具有共特征的有血有肉的人抽象成人类

  • 属性:类具有的共同特征;如人类都有手、脚、眼睛、姓名、性别、年龄等

  • 方法:类具有的共同行为;如人类都能说话、吃饭、睡觉等

  • 对象:类的实例;如上述说的张三、李四、王五等实实在在的人员

    注意:类是对象的模板,对象是类的实例,一个类可以创建无数个对象

类和对象的关系

类和对象的关系

面向对象三大特征

  • 封装,Encapsulation

  • 继承,Inheritance

  • 多态,Polymorphism

    后面会展开单独讲解

面向对象示例

  • 下图就是一个典型的面向对象的UML(Unified Modeling Language)类图,体现了上述的面向对象的三大特征,后面将会围绕此示例进行讲解

    面向对象基础-示例

定义类

  • Java是面向对象的语言,所有的代码,都必须写在类中

  • 关键字是class

  • 类主要包含

    • 属性,即特征,可以有0~n个属性
    • 方法,即行为,可以有0~n个方法
  • 语法规则

    访问修饰符 [其他修饰符] class 类名称{
    //定义属性,也叫成员变量
    访问修饰符 数据类型 属性名称;
    ... //可定义多个属性

    //定义方法
    访问修饰符 [其他修饰符] 返回值数据类型 方法(参数1,参数2 ...){
    //方法休
    return 表达式;
    }
    ... //可定义多个方法
    }

    语法解释:

    • class:类定义关键字
    • 类名称:遵守关键字定义规则,使用大驼峰,如MyClass、OrderDetail等
    • []:表示可选
    • 访问修改符:包括public、protected、(default)、private,后面会讲解,暂时都使用public
    • 其他修改符:包括static、abstract等,后面会讲解,暂时不使用
    • 类体:接在类名称后,使用{}包括的部分
    • 属性:参考定义变量中的成员变量,只是多了访问修饰符
    • 方法:参考定义方法,支持普通方法重载,遵守方法重载规则
  • 应用实例

    • 应用实例1,定义示例中的工作人员类

      package com.bjpowernode.demo.school;

      /**
      * 工作人员类
      */
      public class Employee {
      //属性,也叫成员变量
      //姓名
      public String name;
      //性别
      public char sex;
      //年龄
      public int age;
      //密码
      public String password;

      //方法
      /**
      * 吃饭
      *
      * @param foodName 食物名称
      */
      public void eatFood(String foodName) {
      System.out.println(this.name + "吃了" + foodName + "。");
      }

      /**
      * 吃饭,普通方法重载
      * @param foodName 食物名称
      * @param drinkName 饮料名称
      */
      public void eatFood(String foodName,String drinkName) {
      System.out.println(this.name + "吃了" + foodName + ",喝了" + drinkName + "。");
      }

      /**
      * 睡觉
      *
      * @param withOthers 一起睡的人
      */
      public void sleep(String[] withOthers) {
      String result;
      //如果withOther不为空且长度大于0,表示和别人睡觉;否则,表示一个人睡觉
      if (withOthers != null && withOthers.length > 0) {
      String others = "";
      //循环遍历拼接睡觉人,方式1
      //使用foreach循环遍历
      for (String withOther : withOthers) {
      //拼接睡觉人,尾部加个逗号
      others = others + withOther + ",";
      }
      //去掉尾部多余的逗号
      others = others.substring(0, others.length() - 1);
      // 循环遍历拼接睡觉人,方式2,与方式1有相同效果使用了String的方法【扩展】
      // others = String.join(",",withOthers);

      result = this.name + "和[" + others + "]一起睡觉。";
      } else {
      result = this.name + "一个人睡觉。";
      }

      //输出
      System.out.println(result);
      }

      /**
      * 汇报工作
      */
      public String report() {
      String result;

      System.out.println(this.name + "汇报工作中...");
      //模拟汇报业务逻辑,如果性别为女,通过;否则,不通过
      if (this.sex == '女') {
      result = "通过";
      } else {
      result = "不通过";
      }
      System.out.println(this.name + "汇报完成。");

      return result;
      }
      }

使用类

  • 即生成类的对象,所以先有类再有对象

  • 关键字是new

  • 遵守变量定义规则,只是类型由基本数据类型变成了自定义的类

  • 语法规则

    • 方式1,定义引用,同时赋值
    //定义变量(引用),同时赋值
    类名称 引用名称 = new 类名称();
    • 方式2,定义引用,后续单独赋值
    //定义变量(引用)
    类名称 引用名称;
    //给变量赋值
    引用名称 = new 类名称();
    //修改变量值
    引用名称 = new 类名称();

    语法解释:

    • new:生成类对象关键字
    • 类名称:引用名称前的类名称和new后而的类名称,两个类名称必须一致;但不一定相同,后面会讲解
    • 引用名称:也叫变量,遵守标识符定义规则,使用小驼峰
  • 应用实例

    • 实例1,测试上述的工作人员类,设置其属性、调用其方法;注意:查看out目录,会在编译后,生成两个.class文件,分别是Employee.class和EmployeeTest.class

      package com.bjpowernode.demo;

      import com.bjpowernode.demo.Employee;

      /**
      * 学校业务测试类
      */
      public class EmployeeTest {
      /**
      * 入口方法
      *
      * @param args
      */
      public static void main(String[] args) {
      //工作人员1
      System.out.println("------------工作人员1------------");
      //定义引用,并指向新建的对象
      Employee zs = new Employee();
      //设置属性
      zs.name = "张三";
      zs.sex = '男';
      zs.age = 25;
      zs.password = "123456";
      //使用属性,输出员工信息
      System.out.println("当前工作人员信息:[" + zs.name + "," + zs.sex + "," + zs.age + "]");
      //使用方法
      zs.eatFood("白米饭"); //吃饭
      zs.sleep(new String[]{"老婆", "孩子"}); //睡觉
      String zsResult = zs.report(); //汇报
      System.out.println(zs.name + "的汇报结果:" + zsResult);

      //工作人员2
      System.out.println("------------工作人员2------------");
      //定义引用
      Employee ls;
      //指向新建的对象
      ls = new Employee();
      //设置属性
      ls.name = "李四";
      ls.sex = '女';
      ls.age = 18;
      zs.password = "111111";
      //使用属性,输出员工信息
      System.out.println("当前工作人员信息:[" + ls.name + "," + ls.sex + "," + ls.age + "]");
      ls.eatFood("面包","葡萄酒");
      ls.sleep(null);
      String lsResult = ls.report();
      System.out.println(ls.name + "的汇报结果:" + zsResult);
      }
      }

【练习】

  1. 练习实例内容,完成代码编写;掌握定义类和使用类

  2. 编写程序,定义汽车类Car,要求如下:

    1. 具有车牌、品牌、颜色、价格、使用年限属性
    2. 具备显示车辆详细信息的方法,在方法中输出当前车辆对象的车牌、品牌、颜色、价格和使用年限
    3. 具备驾驶方法,形式参数为驾驶人,在方法中输出"驾驶人正在驾驶xx品牌炸街。";如当前对象的品牌是"老头乐",驾驶方法的输入参数为"张二爷",则输出"张二爷正在驾驶老头乐炸街。"

    定义一个CarTest类,在main方法中定义类对象,给属性赋值,并调用方法

  3. 编写程序,定义图书信息类BookInformation,要求如下

    1. 具有编号、名称、作者、价格、页数属性
    2. 具备显示图书信息的方法,在方法中输出当前对象的编号、名称、作者、价格、页数
    3. 具备翻页方法,形式参数为要翻页的页号,其逻辑为:
      1. 页号为负数,提示:翻页失败,页号错误
      2. 页号小于页数,提示:翻页成功,成功翻到xx页
      3. 页号超过对象页数,提示:翻页失败,超过书箱的最大页数

    定义一个BookInformationTest类,在main方法中定义类对象,给属性赋值,并调用方法

  4. 编写程序,定义学校类School,要求如下【扩展】:

    1. 具有编号、校名、类型(是幼儿园、小学、初中、高中、大学)、员工列表等属性;其中员工列表是上述应用实例中的Employee类型数组
    2. 具备输出员工信息方法,接收形式参数为性别,根据参数输出该性别的员工的详细信息
    3. 具备获取员工平均年龄的方法,返回平均年龄

    另外,定义一个SchoolTest类,在main方法中定义类对象,给属性赋值,并调用方法

属性默认值

  • 区别于局部变量,成员变量(属性)定义后,哪怕不赋初始值,也会有一个默认值

  • 不同数据类型的默认值,并不相同,具体如下

    面向对象基础-默认值

    不同数据类型的成员变量默认值

    注意:所有非8种基本数据类型的都为引用数据类型,像Java中的类、第三方类、自定义类、数组、枚举等,默认值都为null

  • 应用实例

    • 应用实例1,不同数据类型的成员变量默认值

      package com.bjpowernode.demo;

      /**
      * 不同数据类型的成员变量默认值
      */
      public class DefaultDemo {
      //成员变量
      public byte byteVariable;
      public short shortVariable;
      public int intVariable;
      public long longVariable;
      public float floatVariable;
      public double doubleVariable;
      public char charVariable;
      public boolean booleanVariable;
      public String stringVariable;
      public Student studentVariable;

      /**
      * 入口方法,可以放置在项目的任意类中,不影响类本身的属性与普通方法
      * @param args
      */
      public static void main(String[] args) {
      //创建对象
      DefaultDemo defaultDemo = new DefaultDemo();

      //输出默认值
      System.out.println("byte:"+defaultDemo.byteVariable);
      System.out.println("short:"+defaultDemo.shortVariable);
      System.out.println("int:"+defaultDemo.intVariable);
      System.out.println("long:"+defaultDemo.longVariable);
      System.out.println("float:"+defaultDemo.floatVariable);
      System.out.println("double:"+defaultDemo.doubleVariable);
      System.out.println("char:"+defaultDemo.charVariable);
      System.out.println("boolean:"+defaultDemo.booleanVariable);
      System.out.println("String:"+defaultDemo.stringVariable);
      System.out.println("Student:"+defaultDemo.studentVariable);
      }
      }

      输出的内容为:

      面向对象基础-默认值实例

构造方法

概述

  • 构造方法是一种特殊的方法
  • 主要用于创建对象时调用,做一些对象必要的初始化操作,多用于给属性设置初始值;就如女娲造人,模子一样,捏人的时候都,基本都是有胳膊有腿的
  • 定义类时,如果不提供自定义构造方法,Java会提供一个默认无参构造方法
  • 但,如果定义了一个有参构造方法,Java就不再提供默认无参构造方法;如果还需要,可以自己定义一个
  • 存在多个构造方法重载时,一般在创建对象时,要指定一个构造方法

语法规则

  • 与普通方法类似

  • 但,有些不一样:

    • 方法名称与类名称完全相同
    • 没有返回值类型
    • 方法体中一般不需要return
    • 不能像普通方法一样通过”对象.方法“进行多次调用,是在创建对象的时候被调用一次
  • 语法规则

    访问修饰符 [其他修饰符] 类名称([参数类型 参数名,参数类型 参数名,...]){
    //方法体
    }

定义构造方法

  • 无参构造方法(也是默认构造方法)

    public 类名称(){
    //方法体
    }
  • 有参数构造方法,可多个,多个属于构造方法重载,遵守方法重载规则

    public 类名称(参数1,参数2){
    //方法体
    }

使用构造方法

  • 在使用new关键字生成对象的同时,选择一个合适的构造方法

  • 使用方法如下

    //新建对象,使用上述的默认构造方法
    类名称 对象引用名 = new 类名称();
    //新建对象,使用上述带参构造方法
    类名称 对象引用名 = new 类名称(值1,值2);
  • 应用实例

    • 实例1,分别定义多个构造方法,使用不同构造方法创建对象,查看属性值

      定义构造方法

      package com.bjpowernode.demo;

      /**
      * 工作人员类
      */
      public class Employee {
      //属性,也叫成员变量
      //姓名
      public String name;
      //性别
      public char sex;
      //年龄
      public int age;
      //密码
      public String password;


      //构造方法
      /**
      * 无参构造方法,如果不定义其他构造方法,此方法默认提供;如果定义了其他构造方法,不再默认提供,需要手写提供
      * 【构造方法重载】
      */
      public Employee() {

      }

      /**
      * 全参数构造方法,多用于初始化所有属性
      * 【构造方法重载】
      * @param name 姓名
      * @param sex 性别
      * @param age 年龄
      */
      public Employee(String name, char sex, int age, String password) {
      this.name = name; //this.name表示当前对象的属性,name表示当前构造传递过来的局部变量
      this.sex = sex;
      this.age = age;
      this.password = password;
      }

      /**
      * 部分参数构造方法,多用于初始化部分属性
      * @param name 姓名
      */
      public Employee(String name){
      this.name = name;
      }

      //方法
      /**
      * 吃饭
      *
      * @param foodName 食物名称
      */
      public void eatFood(String foodName) {
      System.out.println(this.name + "吃了" + foodName + "。");
      }

      /**
      * 吃饭,普通方法重载
      * @param foodName 食物名称
      * @param drinkName 饮料名称
      */
      public void eatFood(String foodName,String drinkName) {
      System.out.println(this.name + "吃了" + foodName + ",喝了" + drinkName + "。");
      }

      /**
      * 睡觉
      *
      * @param withOthers 一起睡的人
      */
      public void sleep(String[] withOthers) {
      String result;
      //如果withOther不为空且长度大于0,表示和别人睡觉;否则,表示一个人睡觉
      if (withOthers != null && withOthers.length > 0) {
      String others = "";
      //循环遍历拼接睡觉人,方式1
      //使用foreach循环遍历
      for (String withOther : withOthers) {
      //拼接睡觉人,尾部加个逗号
      others = others + withOther + ",";
      }
      //去掉尾部多余的逗号
      others = others.substring(0, others.length() - 1);
      // 循环遍历拼接睡觉人,方式2,与方式1有相同效果使用了String的方法【扩展】
      // others = String.join(",",withOthers);

      result = this.name + "和[" + others + "]一起睡觉。";
      } else {
      result = this.name + "一个人睡觉。";
      }

      //输出
      System.out.println(result);
      }

      /**
      * 汇报工作
      */
      public String report() {
      String result;

      System.out.println(this.name + "汇报工作中...");
      //模拟汇报业务逻辑,如果性别为女,通过;否则,不通过
      if (this.sex == '女') {
      result = "通过";
      } else {
      result = "不通过";
      }
      System.out.println(this.name + "汇报完成。");

      return result;
      }
      }

      使用构造方法

      package com.bjpowernode.demo;

      public class EmployeeTest {

      public static void main(String[] args) {
      //使用类
      //1、使用无参构造方法
      System.out.println("--------------使用无参构造方法--------------");
      //使用无参构造方法
      Employee e1 = new Employee();
      //给属性赋值
      e1.name = "张三";
      e1.sex = '女';
      e1.age = 28;
      e1.password = "123456";
      //调用方法
      e1.eatFood("牛排", "红酒");
      String[] others = {"男人", "小孩"};
      e1.sleep(others);
      String e1Result = e1.report();
      System.out.println(e1.name + "汇报的结果:" + e1Result);

      //2、使用全参数构造方法
      System.out.println("--------------使用全参数构造方法--------------");
      //使用全参数构造方法
      Employee e2 = new Employee("李四",'男',35,"999999");
      e2.eatFood("丝瓜", "山泉水");
      e2.sleep(null);
      String e2Result = e2.report();
      System.out.println(e2.name + "汇报的结果:" + e2Result);

      //3、使用部分参数构造方法
      System.out.println("--------------使用部分参数构造方法--------------");
      //使用部分参数构造方法
      Employee e3 = new Employee("王小二");
      //给属性赋值
      e3.sex = '女';
      e3.age = 45;
      e3.password = "111111";
      //调用方法
      e3.eatFood("汉堡", "可乐");
      String[] e3Other = {"老公"};
      e3.sleep(e3Other);
      String e3Result = e3.report();
      System.out.println(e1.name + "汇报的结果:" + e3Result);
      }
      }

【练习】

  1. 提问

    1. 下面的构造方法定义正确吗?应该如何修改?

      public void ConstructorDemo(){
      //方法体
      }
    2. 下面的代码是否正确?应该如何修改?

      package com.bjpowernode.demo;

      /**
      * 构造方法
      */
      public class ConstructorDemo {
      //成员变量
      public int no;
      public String name;

      /**
      * 有参构造方法
      */
      public ConstructorDemo(int no,String name){
      this.no = no;
      this.name = name;
      }

      /**
      * 入口方法
      * @param args
      */
      public static void main(String[] args) {
      //创建对象
      ConstructorDemo constructorDemo = new ConstructorDemo();
      }
      }
  2. 练习实例内容,完成代码编写;掌握默认值、构造方法的定义和使用

  3. 重构上一个练习的Car类,添加一个无参构造方法、一个全部参数初始化构造方法;在CarTest类中使用每个构造方法进行测试

  4. 重构 上一个练习的BookInformation类,添加一个无参构造方法、一个只初始化编号和名称的构造方法、一个全部参数初始化构造方法;在BookInformationTest类中使用每个构造方法进行测试

  5. 重构上一个练习的School类,添加一个无参构造方法、一个只初始化员工列表的构造方法、一个全部参数初始化构造方法;在SchoolTest类中使用每个构造方法进行测试

对象内存分配

  • 在程序运行过程中,JVM负责资源分配,主要分配资源的地方在栈和堆中,如下图

    方法-JVM

    JVM结构
  • 分配资源,主要针对变量、引用、对象、常量等进行,如下图

    • 局部变量:分配在栈中
    • 局部引用:分配在栈中
    • 对象:分配在堆中
    • 常量:分配在方法区(元空间)

    面向对象基础-JVM资源分配

  • 并且,堆空间相对较大,可配置;针对堆中的资源分配,由JVM进行,并有GC(Garbage Collection,垃圾回收机制)支持

  • 应用实例

    • 实例1,对象空间分配

      • 实例代码

        package com.bjpowernode.demo;

        /**
        * 资源分配
        */
        public class StudentTest {
        /**
        * 入口方法
        * @param args
        */
        public static void main(String[] args) {
        //定义基本数据类型局部变量,变量本身和值都存放于栈中
        int i = 10;

        //定义引用数据类型局部变量,也叫局部引用,引用s1存放于栈中,new Student()生成的对象存放于堆中,并将引用s1指向这个堆中这个对象
        Student s1 = new Student();

        //定义引用数据类型局部变量,也收局部引用,引用s2存放于栈中
        Student s2;
        //new Student()生成的对象存放于堆中,并将引用s2指向这个堆中这个对象
        s2=new Student();
        }
        }
      • 资源分配结果

        面向对象基础-JVM资源分配实例

        注意:s1和s2赋值后面的语句是一样,但是两个完全不同的对象

空指针异常

  • 从上面内存分配可知,8种基本数据类型是直接存储字面量

  • 区别于基本数据类型,引用数据类型存储的是对象在堆中的地址,需要指向实际的对象;然后,通过”引用.属性“、”引用.方法“来使用指向 的对象的属性和方法

  • 如果引用数据类型的变量没有指向实际的对象,则其值为null;此时,不能访问该变量的子属性和方法

  • 但是,在程序中,如果只定义了引用,没有将引用指向对象,将有可能产生空指针异常(NullPointerException),也俗称NPE

  • 这是初学者开发过程中和项目过程中常见的异常

  • 应用实例

    • 实例1,体验空指针异常

      实例代码

      package com.bjpowernode.demo;

      /**
      * 空指针异常实例
      */
      public class NpeDemo {
      //成员变量
      int no;
      String name;
      Student student;
      Student[] students;
      int[] ids;

      /**
      * 入口方法
      *
      * @param args
      */
      public static void main(String[] args) {
      //【局部变量】
      //String类型空指针异常
      String myName = null;
      myName.length(); //将会产生[空指针异常]
      //自定义类型空指针异常
      Student student = null;
      student.study("数学"); //将会产生[空指针异常]

      //【成员变量】
      NpeDemo npeDemo = new NpeDemo();
      System.out.println(npeDemo.no); //正常访问,使用默认值
      npeDemo.name.length(); //将会产生[空指针异常]
      npeDemo.student.study("数学"); //将会产生[空指针异常]
      npeDemo.students[0].study("语言"); //将会产生[空指针异常]
      System.out.println(npeDemo.ids[0]); //将会产[生空指针异常]
      }
      }

      输出结果,运行代码查看

【练习】

  1. 练习实例内容,熟悉空指针产生的原因与特点,并记住这个常见的异常类,熟悉通过异常栈找到问题的代码行数

对象比较

  • 8种基本数据类型的数据之间进行比较是否相等时,直接使用比较运算符“==”,比较的是实际的值,简单、方便且效率很高

  • 引用数据类型的对象之间进行比较时,由于对象更复杂,有很多属性和方法,直接使用”==“进行比较对象,比较的是引用指向的对象在堆中的地址是否相同,而不是对象内容本身或其中的属性或方法

  • 应用实例

    • 应用实例1,基本数据类型的==比较

      • 下面的代码,输出是true还是false?

        package com.bjpowernode.demo;

        /**
        * 基本数据类型比较
        */
        public class BaseTypeCompareDemo {
        public static void main(String[] args) {
        //定义基本数据类型变量one
        int one = 10;
        //定义基本数据类型变量two
        int two = 10;

        //比较
        System.out.println(one == two); //输出的是true还是false?

        //定义基本数据类型变量first
        double first = 1.234;
        //定义基本数据类型变量second
        double second = 1.234;

        //比较
        System.out.println(first == second); //输出的是true还是false?
        }
        }
    • 应用实例2,对象的==比较

      • 下面的代码,输出的是true还是false?

        package com.bjpowernode.demo;

        /**
        * 对象使用==比较
        */
        public class ObjectCompareDemo {
        public static void main(String[] args) {
        //定义引用s1,指向新建对象
        Student s1 = new Student();
        //定义引用s2,指向新建对象
        Student s2 = new Student();

        //使用==比较引用s1、s2
        System.out.println(s1==s2); //输出的是true还是false?
        }
        }
      • 下面的代码,输出的是true还是false?

        package com.bjpowernode.demo;

        /**
        * 对象使用==比较
        */
        public class ObjectCompareDemo {
        public static void main(String[] args) {
        //定义引用s1,指向新建对象
        Student s1 = new Student();
        //定义引用s2,指向s1
        Student s2 = s1;

        //使用==比较引用s1、s2
        System.out.println(s1==s2); //输出的是true还是false?
        }
        }

【练习】

  1. 练习示例内容,熟悉对象使用==比较的特点
  2. 两个值为null的引用使用==比较结果是true还是false?其中一个不为null呢?

字符串的==和equals对比

  • 字符串也是引用类型,==比较也遵守上面的对象比较规则

  • 但,使用字符串字面量并不全遵守引用数据类型对象比较规则

  • 应用实例

    • 应用实例1,字符串使用==比较

      • 下面的代码,输出的是true还是false?

        package com.bjpowernode.demo;

        /**
        * 字符串使用==比较
        */
        public class StringDemo {
        /**
        * 入口方法
        * @param args
        */
        public static void main(String[] args) {
        //定义s1,指向新建对象
        String s1 = new String("abc");
        //定义s2,指向新建对象
        String s2 = new String("abc");

        //使用
        System.out.println(s1 == s2); //输出的是true还是false?
        }
        }
      • 下面的代码,输出的是true,还是false?

        package com.bjpowernode.demo;

        /**
        * 字符串使用==比较
        */
        public class StringDemo {
        /**
        * 入口方法
        * @param args
        */
        public static void main(String[] args) {
        //定义s1,指向新建对象
        String s1 = "abc";
        //定义s2,指向新建对象
        String s2 = "abc";

        //使用
        System.out.println(s1 == s2); //输出的是true还是false?
        }
        }
  • 上面的应用实例中,只是s1、s2定义的方式不同,但使用”==“进行对象比较的结果并不相同,是因字符串常量池在做怪

  • 字符串常量池

    • 产生原因:在程序中对字符串的定义、使用、连接操作非常多,会产生很多内容相同的字符串对象,浪费空间、影响程序运行效率

    • 存放位置:位于JVM堆中的一块独立区域,只存放字符串的字面量

    • 定义方式:

      //定义字符串的引用s1,指向了字符串字面量,该字面量会存储于【字符串常量池】
      String s1 = "abc";
    • 原理:

      • 当新定义一个字符串引用,如s1,使用某个字符串常量时,如"abc"
      • 先去字符串常量池中查看是否已经有”abc"这个字符串字面量,如果有,直接返回地址赋值给引用s1
      • 如果在字符串常量池中没有“abc”这个字符串字面量,则往字符串常量池添加这个字面量,然后再返回地址赋值给引用s1
    • 字符串字面量与new String()方式使用对比:

      package com.bjpowernode.demo;

      /**
      * 字符串字面量与new String()方式使用对比
      */
      public class StringDemo {
      /**
      * 入口方法
      *
      * @param args
      */
      public static void main(String[] args) {
      //字符串引用s1直接使用字符串字面量赋值,字面量"abc"将存储于【字符串常量池】中
      String s1 = "abc";
      //同上
      String s2 = "abc";

      //字符串引用s3使用新建对象赋值,与普通对象定义与赋值相同;"abc"将存储于【堆】中
      String s3 = new String("abc");
      //同s3
      String s4 = new String("abc");

      //输出
      System.out.println(s1 == s2); //true
      System.out.println(s1 == s3); //false
      System.out.println(s1 == s4); //false
      System.out.println(s2 == s3); //false
      System.out.println(s2 == s4); //false
      System.out.println(s3 == s4); //false
      }
      }
  • equals方法

    • 这是Object对象的一个方法

    • 在String类型中进行了覆盖,定义了比较的逻辑,直接比较的内容

    • 由于String是使用char[]保存的,故比较的逻辑是一个一个char进行比较

    • 此时,就不管实际内容是存储于堆,还是字符串常量池,比较的都是实际的char[]的值,因此会相等

    • 应用实例

      • 应用实例1,使用equals进行比较

        package com.bjpowernode.demo;

        /**
        * 字符串使用equals进行比较
        */
        public class StringDemo {
        /**
        * 入口方法
        *
        * @param args
        */
        public static void main(String[] args) {
        //字符串引用s1直接使用字符串字面量赋值,字面量"abc"将存储于【字符串常量池】中
        String s1 = "abc";
        //同上
        String s2 = "abc";

        //字符串引用s3使用新建对象赋值,与普通对象定义与赋值相同;"abc"将存储于【堆】中
        String s3 = new String("abc");
        //同s3
        String s4 = new String("abc");

        //输出
        System.out.println(s1.equals(s2)); //true
        System.out.println(s1.equals(s3)); //true
        System.out.println(s1.equals(s4)); //true
        System.out.println(s2.equals(s3)); //true
        System.out.println(s2.equals(s4)); //true
        System.out.println(s3.equals(s4)); //true
        }
        }
  • 原则:字符串String比较时,推荐使用equals方法

  • 字符串连接的一些特点【扩展】

    1. 字符串连接运算中,字面量与字面量的连接结果在常量池,会在编译进进行优化
    2. 字符串连接运算中,只要其中有一个是变量,结果就在堆中,而不是字符串常量池中;如果有字面量与变量连接,相当于在堆中new String()定义了新的对象;如果全是变量连接,相当于使用了StringBuilder(后面会讲解)
    3. 连接结果调用intern()方法,则会将堆中字符串对象放入字符串常量池,并返回此对象地址

    应用实例

    package com.bjpowernode.demo;

    /**
    * 字符串连接的一些特点
    */
    public class StringPoolDemo {
    public static void main(String[] args) {
    //特点1:字符串连接运算中,字面量与字面量的连接结果在常量池,会在编译进进行优化
    System.out.println("-----------特点1-----------");
    String s1 = "a" + "b" + "c"; //会直接优化成String s1="abc";
    String s2 = "abc";
    System.out.println(s1 == s2); //true

    //特点2:字符串连接运算中,只要其中有一个是变量,结果就在堆中,而不是字符串常量池中
    System.out.println("-----------特点2-----------");
    String fullName = "尼古拉斯赵四";
    String firstName = "尼古拉斯";
    String lastName = "赵四";
    String fullNameConnect1 = "尼古拉斯" + "赵四"; //会直接优化成String fullNameConnect1="尼古拉斯赵四";
    String fullNameConnect2 = firstName + lastName; //相当于使用了StringBuilder
    String fullNameConnect3 = firstName + "赵四"; //相当于在堆中new String()定义了新的对象
    String fullNameConnect4 = "尼古拉斯" + lastName; //相当于在堆中new String()定义了新的对象
    System.out.println(fullName == fullNameConnect1); //true
    System.out.println(fullName == fullNameConnect2); //false
    System.out.println(fullName == fullNameConnect3); //false
    System.out.println(fullName == fullNameConnect4); //false

    //特点3:连接结果调用intern()方法,则会将堆中字符串对象放入字符串常量池,并返回此对象地址
    System.out.println("-----------特点3-----------");
    String fullNameConnect2Intern = fullNameConnect2.intern();
    System.out.println(fullName == fullNameConnect2Intern);

    //特点4:如果是常量,则常量的连接,也会在编译期优化,【了解】
    System.out.println("-----------特点4-----------");
    String fullN = "尼古拉斯赵四";
    final String firstN = "尼古拉斯";
    final String lastN = "赵四";
    String fullNConnect = firstN + lastN; //编译时会进行优化
    System.out.println(fullN == fullNConnect);
    }
    }

    注意:字符串的处理和字符 串常量池在不同的JDK版本中有不同的特性

【练习】

  1. 练习实例内容,熟悉字符串的==和equals对比

实战和作业

  1. 编写程序,描述人的特征与行为
    1. 定义人类(Person),具有身份证号、姓名、性别、年龄、身高等属性;具备吃饭、睡觉、打豆豆等方法
    2. 定义人类应用类(PersonApp),测试人类,生成多个人类对象,并设置属性值、使用属性值、调用方法
  2. 编写程序,描述一个计算器的特征与行为
    1. 定义计算器类(Calculator),具有操作1、操作数2两个属性;具有加、减、乘、除四个方法,使用操作数1、操作数2完成方法逻辑
    2. 定义计算器测试类(CalculatorTest),利用计算器类进行进行算术运算
  3. 编写程序,描述一个电脑的特征与行为
    1. 定义主板类
      1. 属性:型号、厂商、价格等
      2. 构造方法:无参构造、全参构造
      3. 普通方法:加电方法(逻辑自定义)、断电方法(逻辑自定义)、输出主板所有信息方法
    2. 定义处理器类
      1. 属性:型号、厂商、价格、内核数量、频率
      2. 构造方法:无参构造、全参构造
      3. 普通方法:开始处理方法(逻辑自定义)、结束处理方法(逻辑自定义)、输出处理器所有信息方法
    3. 定义内存类
      1. 属性:型号、厂商、价格、容量、频率
      2. 构造方法:无参构造、全参构造
      3. 普通方法:开始存储方法(逻辑自定义)、释放存储方法(逻辑自定义)、输出内存所有信息方法
    4. 定义电脑类
      1. 属性:型号、名称、厂商、价格、保修周期(如3年)、主板、处理器、内存(可以是多个)
      2. 构造方法:无参构造、全参构造
      3. 普通方法:
        1. 开机方法:逻辑自定义,在其中会调用主板的加电方法、处理器的开始处理方法、内存的开始存储方法
        2. 关机方法:逻辑自定义,在其中会调用主板的断电方法、处理器的结束处理方法、内存的释放存储方法
        3. 显示电脑信息方法:显示电脑信息和里面的主板信息、处理器信息、内存信息
    5. 定义电脑测试类,组织出上述的电脑类对象,并调用开机方法、关机方法、显示电脑信息方法进行测试
  4. 编写程序,要求如下
    1. 定义丈夫类(Husband)
      1. 具有身份证号、姓名、妻子(Wife类型)属性
      2. 具备无参和有参构造方法
      3. 具备获取妻子姓名的普通方法
    2. 定义妻子类(Wife)
      1. 具有身份证号、姓名、丈夫(Husband类型)属性
      2. 具备无参和有参构造方法
      3. 具备获取丈夫姓名的普通方法
    3. 定义测试类AppTest类,
      1. 分别生成丈夫对象、妻子对象
      2. 使用丈夫对象的获取妻子姓名的普通方法获取其妻子姓名并输出
      3. 使用妻子对象的获取丈夫姓名的普通方法获取其丈夫姓名并输出