读书笔记:Java 编程的逻辑(三)

面向对象

第3章 类的基础

  • Java定义了 8 种基本数据类型:4 种整型 byte、short、int、long,两种浮点类型 foat、double,一种真假类型 boolean,一种字符类型 char。其他类型的数据都用类这个概念表达。

3.1 类的基本概念

3.1.1 函数容器
  • static 表示类方法,也叫静态方法,与类方法相对的是实例方法。
    • 实例方法没有 static 修饰符,必须通过实例或者对象调用,而类方法可以直接通过类名进行调用,不需要创建实例。
    • public 表示这些函数是公开的,可以在任何地方被外部调用。
    • 与 public 相对的是 private。如果是 private,则表示私有,这个函数只能在同一个类内被别的函数调用,而不能被外部的类调用
  • 将函数声明为 private 可以避免该函数被外部类误用,调用者可以清楚地知道哪些函数是可以调用的,哪些是不可以调用的。
  • 类实现者通过 private 函数封装和隐藏内部实现细节,而调用者只需要关心 public 就可以了。可以说,通过 private 封装和隐藏内部实现细节,避免被误操作,是计算机程序的一种基本思维方式。
3.1.2 自定义数据类型
  • 我们将类看作自定义数据类型,所谓自定义数据类型就是除了 8 种基本类型以外的其他类型,用于表示和处理基本类型以外的其他数据。
    • 一个数据类型由其包含的属性以及该类型可以进行的操作组成,属性又可以分为是类型本身具有的属性,还是一个具体实例具有的属性,同样,操作也可以分为是类型本身可以进行的操作,还是一个具体实例可以进行的操作。
    • 一个数据类型就主要由 4 部分组成:
      • 类型本身具有的属性,通过类变量体现。
      • 类型本身可以进行的操作,通过类方法体现。
      • 类型实例具有的属性,通过实例变量体现。
      • 类型实例可以进行的操作,通过实例方法体现。
    • 不过,对于一个具体类型,每一个部分不一定都有,Arrays 类就只有类方法。
    • 类变量和实例变量都叫成员变量,也就是类的成员,类变量也叫静态变量或静态成员变量。
    • 类方法和实例方法都叫成员方法,也都是类的成员,类方法也叫静态方法。
  • 类变量
    • 类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量。
    • 与类方法一样,类变量可以直接通过类名访问,如 Math.PI。
    • final 在修饰变量的时候表示常量,即变量赋值后就不能再修改了。
  • 实例变量和实例方法
    • 实例变量表示具体的实例所具有的属性,实例方法表示具体的实例可以进行的操作。
3.1.3 定义第一个类
  • 我们定义一个简单的类,表示在平面坐标轴中的一个点:
    class Point {
        public int x;
        public int y;
        public double distance() {
            return Math.sqrt(x * x + y * y);
        }
    }
    
    • 第一行表示类型的名字是 Point,是可以被外部公开访问的。
      • 修饰符可以没有(即留空),表示一种包级别的可见性。
      • 另外,类可以定义在一个类的内部,这时可以使用 private 修饰符。
    • 第二三行定义了两个实例变量 x 和 y,分别表示 x 坐标和 y 坐标,实例变量不能有 static 修饰符。
    • 第四行定义了实例方法 distance,表示该点到坐标原点的距离。
      • 该方法可以直接访问实例变量 x 和 y,这是实例方法和类方法的最大区别。
      • 实例方法直接访问实例变量,到底是什么意思呢?其实,在实例方法中,有一个隐含的参数,这个参数就是当前操作的实例自己,直接操作实例变量,实际也需要通过参数进行。
      • 类方法只能访问类变量,不能访问实例变量,可以调用其他的类方法,不能调用实例方法。
      • 实例方法既能访问实例变量,也能访问类变量,既可以调用实例方法,也可以调用类方法。
3.1.4 使用第一个类
  • 定义了类本身和定义了一个函数类似,本身不会做什么事情,不会分配内存,也不会执行代码。
  • 方法要执行需要被调用,而实例方法被调用,首先需要一个实例,实例也称为对象。
    public static void main(String[] args) {
    	Point p = new Point();
    	p.x = 3;
    	p.y = 4;
    	System.out.println(p.distance());
    }
    
    • 第二行包含了 Point 类型的变量声明和赋值,它可以分为两部分:
      • Point p 声明了一个变量,这个变量叫 p,是 Point 类型的。
        • 这个变量和数组变量是类似的,都有两块内存:一块存放实际内容,一块存放实际内容的位置。
        • 声明变量本身只会分配存放位置的内存空间,这块空间还没有指向任何实际内容。
        • 因为这种变量和数组变量本身不存储数据,而只是存储实际内容的位置,它们也都称为引用类型的变量。
        • p = new Point();创建了一个实例或对象,然后赋值给了 Point 类型的变量 p,它至少做了两件事:1)分配内存,以存储新对象的数据,对象数据包括这个对象的属性,具体包括其实例变量 x 和 y。2)给实例变量设置默认值,int 类型默认值为 0。
        • 与方法内定义的局部变量不同,在创建对象的时候,所有的实例变量都会分配一个默认值,这与创建数组的时候是类似的,数值类型变量的默认值是 0,boolean 是 false,char 是“\u0000”,引用类型变量都是null。null是一个特殊的值,表示不指向任何对象。这些默认值可以修改。
    • 第三四行给对象的变量赋值,语法形式是:<对象变量名>.<成员名>
    • 第五行调用实例方法 distance,并输出结果,语法形式是:<对象变量名>.<方法名>实例方法内对实例变量的操作,实际操作的就是 p 这个对象的数据。
      • 对实例变量和实例方法的访问都通过对象进行,通过对象来访问和操作其内部的数据是一种基本的面向对象思维。
      • 一般而言,不应该将实例变量声明为 public,而只应该通过对象的方法对实例变量进行操作。这也是为了减少误操作,直接访问变量没有办法进行参数检查和控制,而通过方法修改,可以在方法中进行检查。
3.1.5 变量默认值
  • 实例变量都有一个默认值,如果希望修改这个默认值,可以在定义变量的同时就赋值,或者将代码放入初始化代码块中,代码块用 {} 包围:
    int x = 1;
    int y;
    {
    	y = 2;
    }
    

    在新建一个对象的时候,会先调用这个初始化,然后才会执行构造方法中的代码。

  • 静态变量也可以这样初始化:
    static int STATIC_ONE = 1;
    static int STATIC_TWO;
    {
    	STATIC_TWO = 2;
    }
    

    语句外面包了一个 static {},这叫静态初始化代码块。静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次。

3.1.6 private 变量
  • 我们修改一下类的定义,将实例变量定义为 private,通过实例方法来操作变量:
    public class Point {
        private int x;
        private int y;
        
        public void setX(int x) {
            this.x = x;
        }
        
        public void setY(int y) {
            this.y = y;
        }
    
        public int getX() {
            return x;
        }
    
        public int getY() {
            return y;
        }
    
        public double distance() {
            return Math.sqrt(x * x + y * y);
        }
    }
    
    • setⅩ/setY 用于设置实例变量的值,getⅩ/getY 用于获取实例变量的值。
    • this 关键字表示当前实例,在语句 this.x=x;中,this.x 表示实例变量 x,而右边的 x 表示方法参数中的 x。
    • 在实例方法中,有一个隐含的参数,这个参数就是 this,没有歧义的情况下,可以直接访问实例变量,在这个例子中,两个变量名都叫 x,则需要通过加上 this 来消除歧义。
    • 在很多情况下,通过函数调用可以封装内部数据,避免误操作,我们一般还是不将成员变量定义为 public。
      public static void main(String[] args) {
      	Point p = new Point();
      	p.setX(3);
      	p.setY(4);
      	System.out.println(p.distance());
      }
      
3.1.7 构造方法
  • 在初始化对象的时候,有一个更简单的方式对实例变量赋初值,就是构造方法:
    public Point() {
    	this(0, 0);
    }
    
    public Point(int x, int y) {
    	this.x = x;
    	this.y = y;
    }
    
    • 这两个就是构造方法,构造方法可以有多个。
  • 不同于一般方法,构造方法有一些特殊的地方:
    • 名称是固定的,与类名相同。
    • 没有返回值,也不能有返回值。
    • 构造方法隐含的返回值就是实例本身。
    • 与普通方法一样,构造方法也可以重载。
  • 第一个构造方法中,this(0,0) 的意思是调用第二个构造方法,并传递参数“0, 0”。
    • this 表示当前实例,可以通过 this 访问实例变量,用在构造方法中则调用其他构造方法
    • 这个 this 调用必须放在第一行,这个规定也是为了避免误操作。
    • 构造方法是用于初始化对象的,如果要调用别的构造方法,先调别的,然后根据情况自己再做调整,而如果自己先初始化了一部分,再调别的,自己的修改可能就被覆盖了。
    • 这个例子中,不带参数的构造方法通过 this(0,0) 又调用了第二个构造方法,这个调用是多余的,因为 x 和 y 的默认值就是 0,不需要再单独赋值,假如改成别的值则有必要赋值。
  • 默认构造方法
    • 每个类都至少要有一个构造方法,在通过 new 创建对象的过程中会被调用。
    • 但构造方法如果没什么操作要做,可以省略。
    • Java 编译器会自动生成一个默认构造方法,也没有具体操作。
    • 但一旦定义了构造方法,Java 就不会再自动生成默认的。
  • 私有构造方法
    • 构造方法可以是私有方法,即修饰符可以为 private。
      • 不能创建类的实例,类只能被静态访问,如 Math 和 Arrays 类,它们的构造方法就是私有的。
      • 能创建类的实例,但只能被类的静态方法调用。有一种常见的场景:类的对象有但是只能有一个,即单例(单个实例)。在这种场景中,对象是通过静态方法获取的,而静态方法调用私有构造方法创建一个对象,如果对象已经创建过了,就重用这个对象。
      • 只是用来被其他多个构造方法调用,用于减少重复代码。
3.1.8 类和对象的生命周期
  • 在程序运行的时候,当第一次通过 new 创建一个类的对象时,或者直接通过类名访问类变量和类方法时,Java 会将类加载进内存,为这个类分配一块空间,这个空间会包括类的定义、它的变量和方法信息,同时还有类的静态变量,并对静态变量赋初始值。
  • 类加载进内存后,一般不会释放,直到程序结束。一般情况下,类只会加载一次,所以静态变量在内存中只有一份。
  • 当通过 new 创建一个对象的时候,对象产生,在内存中,会存储这个对象的实例变量值,每做 new 操作一次,就会产生一个对象,就会有一份独立的实例变量。
    • 每个对象除了保存实例变量的值外,可以理解为还保存着对应类型即类的地址,这样,通过对象能知道它的类,访问到类的变量和方法代码。
    • 实例方法可以理解为一个静态方法,只是多了一个参数 this。通过对象调用方法,可以理解为就是调用这个静态方法,并将对象作为参数传给 this。
  • 对象的释放是被 Java 用垃圾回收机制管理的,当对象不再被使用的时候会被自动释放。
    • 具体来说,对象和数组一样,有两块内存,保存地址的部分分配在栈中,而保存实际内容的部分分配在堆中。
    • 栈中的内存是自动管理的,函数调用入栈就会分配,而出栈就会释放。
    • 堆中的内存是被垃圾回收机制管理的,当没有活跃变量指向对象的时候,对应的堆空间就可能被释放,具体释放时间是 Java 虚拟机自己决定的。
    • 活跃变量就是已加载的类的类变量,以及栈中所有的变量。

3.3 代码的组织机制

3.3.1 包的概念
  • 使用任何语言进行编程都有一个相同的问题,就是命名冲突。程序一般不全是一个人写的,会调用系统提供的代码、第三方库中的代码、项目中其他人写的代码等,不同的人就不同的目的可能定义同样的类名/接口名,Java中解决这个问题的主要方法就是包。
  • 包有包名,这个名称以点号(.)分隔表示层次结构。
    • 比如,我们之前常用的 String 类就位于包 java.lang 下,其中 java 是上层包名,lang 是下层包名。
    • 带完整包名的类名称为其完全限定名,比如 String 类的完全限定名为 java.lang.String。
    • Java API 中所有的类和接口都位于包 Java 或 javax 下,Java 是标准包,javax 是扩展包。
  • 声明类所在的包
    • 定义类的时候,应该先使用关键字 package 声明其包名。
      • 包声明语句应该位于源代码的最前面,前面不能有注释外的其他语句。
      • 包名和文件目录结构必须匹配,如果不匹配,Java 会提示编译错误。
    • 为避免命名冲突,Java 中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名。
      • 比如,域名是 apache.org,包名就以 org.apache 开头。
      • 如果代码需要公开给其他人用,最好有一个域名以确保唯一性,如果只是内部使用,则确保内部没有其他代码使用该包名即可。
    • 除了避免命名冲突,包也是一种方便组织代码的机制。
      • 一般而言,同一个项目下的所有代码都有一个相同的包前缀,这个前缀是唯一的,不会与其他代码重名。
      • 在项目内部,根据不同目的再细分为子包,子包可能又会分为下一级子包,形成层次结构,内部实现一般位于比较底层的包。
      • 包可以方便模块化开发,不同功能可以位于不同包内,不同开发人员负责不同的包。
      • 包也可以方便封装,供外部使用的类可以放在包的上层,而内部的实现细节则可以放在比较底层的子包内。
  • 通过包使用类
    • 同一个包下的类之间互相引用是不需要包名的,可以直接使用。但如果类不在同一个包内,则必须要知道其所在的包。
    • 使用有两种方式:一种是通过类的完全限定名;另外一种是将用到的类引入当前类。
      • 只有一个例外,java.lang 包下的类可以直接使用,不需要引入,也不需要使用完全限定名,比如 String 类、System 类,其他包内的类则不行。
      • 引入的关键字是 import,import 需要放在 package 定义之后,类定义之前。
      • 做 import 操作时,可以一次将某个包下的所有类引入,语法是使用 . ∗ .* .
      • 需要注意的是,这个引入不能递归,它只会引入包的直接类,而不会引入嵌套包内的类。
      • 在一个类内,对其他类的引用必须是唯一确定的,不能有重名的类,如果有,则通过 import 只能引入其中的一个类,其他同名的类则必须要使用完全限定名。
  • 有一种特殊类型的导入,称为静态导入,它有一个 static 关键字,可以直接导入类的公开静态方法和成员。静态导入不应过度使用,否则难以区分访问的是哪个类的代码。
  • 包范围可见性
    • 如果什么修饰符都不写,它的可见性范围就是同一个包内,同一个包内的其他类可以访问,而其他包内的类则不可以访问。需要说明的是,同一个包指的是同一个直接包,子包下的类并不能访问。
    • 除了 public 和 private 修饰符,还有一个与继承有关的修饰符 protected。
    • protected 可见性包括包可见性,也就是说,声明为 protected 不仅表明子类可以访问,还表明同一个包内的其他类可以访问,即使这些类不是子类也可以。
    • 总结来说,可见性范围从小到大是:private < 默认(包) < protected < public
3.3.3 程序的编译与链接
  • 从 Java 源代码到运行的程序,有编译和链接两个步骤。
    • 编译是将源代码文件变成扩展名是 .class 的一种字节码,一般是由 javac 命令完成的。
    • 链接是在运行时动态执行的,.class 文件不能直接运行,运行的是 Java 虚拟机,执行的就是 Java 命令,转换为机器能识别的二进制代码运行。
    • 所谓链接就是根据引用到的类加载相应的字节码并执行。
  • Java编译和运行时,都需要以参数指定一个 classpath,即类路径。
    • 类路径可以有多个,对于直接的 class 文件,路径是 class 文件的根目录;
    • 对于 jar 包,路径是 jar 包的完整名称(包括路径和 jar 包名)。
  • 在 Java 源代码编译时,Java 编译器会确定引用的每个类的完全限定名,确定的方式是根据 import 语句和 classpath。
    • 如果导入的是完全限定类名,则可以直接比较并确定。
    • 如果是模糊导入,则根据 classpath 找对应父包,再在父包下寻找是否有对应的类。
    • 如果多个模糊导入的包下都有同样的类名,则 Java 会提示编译错误,此时应该明确指定导入哪个类。
    • Java 运行时,会根据类的完全限定名寻找并加载类,寻找的方式就是在类路径中寻找,如果是 class 文件的根目录,则直接查看是否有对应的子目录及文件。
    • 如果是 jar 文件,则首先在内存中解压文件,然后再查看是否有对应的类。
    • 总结来说,import 是编译时概念,用于确定完全限定名,在运行时,只根据完全限定名寻找并加载类,编译和运行时都依赖类路径,类路径中的 jar 文件会被解压缩用于寻找和加载类。
  • 在 Java 9 中,清晰地引入了模块的概念,JDK 和 JRE 都按模块化进行了重构,传统的组织机制依然是支持的,但新的应用可以使用模块。
    • 一个应用可由多个模块组成,一个模块可由多个包组成。
    • 模块之间可以有一定的依赖关系,一个模块可以导出包给其他模块用,可以提供服务给其他模块用,也可以使用其他模块提供的包,调用其他模块提供的服务。
    • 对于复杂的应用,模块化有很多好处,比如更强的封装、更为可靠的配置、更为松散的耦合、更动态灵活等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值