• 【转载】Java程序设计入门 (二)


    在上一节中我们学习了如何创建类和对象, 在后半部分还讨论了访问性修饰符的话题. 事实上, 在 Java 中还有两个比较常用的修饰符, 可能会在编程的初级阶段就用到, 所以本节开始部分我们先介绍这两个常用修饰符, 然后…… 再讨论别的话题……

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    本节关键词: final, static, 实例变量, 静态变量, 静态方法, 继承, 覆盖
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    『 final 与 static  
    在开始之前先提个醒: 上节提到的 public, protected, private 修饰符主要是对可访问性(可视性)进行控制, 而现在将要讨论的这两个修饰符并不影响可访问性. 具体是怎样的? 现在就来看看吧……

    final 
    添加了 final 修饰符的属性、方法、类将被认为是"最终版", 若修饰属性, 则表示该属性在初始化之后即不可重新赋值 (类似"常量"); 若修饰方法, 则表示该方法是不可被覆盖(Override)的; 若将其置于类声明之前(关键词class之前), 则表示该类不能被继承.

    先看一个修饰属性的例子:
    修改一下上节例子中的 Pig 类, 为 sex 属性加上 final 修饰符. 如下: ( 我认为性别一旦设定就不能乱变了, 呵呵~)
    final public String sex;
    OK, 现在试试在别的地方给sex赋值一下…… 是不是Eclipse已经提示错误了~

    也许, 你已经想到了一个问题, 既然刚才我们说final用于修饰属性时类似"常量", 那么应该象C语言一样, 在声明的时候就必须赋予初始值呀! 显然, 上面那句代码是没有给 sex 属性赋初值的, 那怎么Eclipse在这个时候为什么又不报错了呢? 
    呵呵, 秘密就在我们上节定义的构造函数中~ ( 如果你到目前为止还没有跟着教程一起练习, 那就太令人伤心了! 本教程是一个系列, 例子会一直沿袭下去……请注意保存自己的劳动成果! )

    Ok, Calm down~
    看到了吧, 我们在构造函数中通过代码 this.sex = sex; 对其进行了初始化. (记住: 构造函数在对象实例化的第一时刻被执行)

    小结一下, final修饰属性时只能在2个地方对该属性赋值(二者取其一):
    (1) 声明同时, 例如: final public String sex = "雄";
    (2) 在构造函数中. 并且, 即使是在构造函数中也只能赋值一次.
    总之…… final修饰的属性, 只能被赋值一次, 之后…… 该属性就是"终态"的了……

    final 还可以放置在方法的参数前 或 方法体中的声明的变量之前, 此时的含意亦与"常量"类似. 例:
    public void f (final String param) {
    
        
    
         final int x = 3;
    
        
    
         param = "Another";      // 不允许
    
         
    
         x = 5;                  // 不允许
    
    }

    关于 final 修饰方法和类的情形, 涉及到"继承", 因到此为止尚未谈及如果实现继承, 暂不举例. 可先记住前述的规则.

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    static
    static 意为"静态"的, 相信学过C语言的同学对它并不陌生.
    这里所谓的"静态", 可姑且将其理解为: 
    (1) 被 static 修饰的东东是从属于类, 而非实例(对象), 因此, 与类同生共灭
    (2) 被 static 修饰的属性将被该类的所有实例(对象)共享 

    呵呵, 很不好理解吧~

    在解释之前我们先定义几个术语, 以便描述:
    (1) 实例变量 : 没有 static 修饰的属性. (之前例子中的name, sex 都是实例变量)
    (2) 静态变量 : 有 static 修饰的属性
    (3) 局部变量 : 定义在方法(函数)体中的变量
    (4) 静态方法 : 有 static 修饰的方法(函数)

    OK, 继续吧……
    没有static修饰的那些属性(实例变量), 事实上它们是属于具体的实例(对象)的, 只有使用 new 关键词将类实例化为对象后, 它们才实际存在, 所以我们说它们是从属于具体对象的,  叫它们"实例变量".
    我们在定义类的时候, 只是声称该类的所有实例(对象)均必须有这些属性(实例变量), 但那个时候还没有任何实例产生, 因此, 这些实例变量也并未真正分配存储空间.

    共享的含意, 可从下图看出:
    Java程序设计入门 (二) - bailey - Baileys Blog
     哎呀~ 画图的水平真是不行, 还是辅以文字说明一下吧……
    上图中, x 为实例变量, 因为当我们通过 new Test() 创建出3个实例(A/B/C)之后, 会产生x的3个独立副本, 可以通过 a.x = 1; b.x = 2; c.x = 3; 分别赋值. 若继续执行 a.x = 4, 只会导致实例A的x值变为4, 而不会影响到实例B和C.
    然而, y 为静态变量, 被实例A/B/C所共享, a.y = b.y = c.y = 5, 若执行 a.y = 6, 则结果变为 a.y = b.y = c.y = 6

    这就是实例变量 与 静态变量的区别……
    所以, 我们可以说 "静态变量" 并不属于并个实例, 而是属于类…… 因此, 在写程序的时候, 我们通常不像上面写 a.y = 6, 而是写成 Test.y = 6 (使用类名引用, 而非对象)

    当 static 被用于修饰某个类的方法时, 该方法就被称作该类的静态方法, 它同样不依附于任何实例. 所以, 即使没有任何实例存在, 也可以使用类似 Test.f(); 的方式来调用静态方法 f 

    对于 static 还有一个特别的用法: 用来修饰一段代码, 姑且称其为"静态代码块"吧, 例如:
    public class Test {
    
        public static int z = 0;
    
        
        static { 
              System.out.println("static 修饰的代码被执行了...");
    
        }
    
    }     
    其中, static { .... }, 大括号内的代码将在首次使用到Test类的时候被自动执行……
    你可以在 HelloJava 中写上一句 System.out.println(Test.z); 运行程序试试~  是不是输出下面的结果了:
    static 修饰的代码被执行了...
    
    0
    呵呵, 有意思吧…… 

    小结一下:
    (1) static 可以修饰 属性(静态变量), 方法 (静态方法), 或一段代码. 但是不能修饰类(不可在类声明之前加static, 这与final不同)
    (2) static 修饰的东西是从属于类的, 生命周期与类相同, 不依赖于实例.
    (3) 正因上述第(2)条所述, "不依赖于实例", 所以…… 有可能静态方法或静态代码块被调用/执行的时候还没有任何实例存在, 因此, 不能在静态方法或静态代码块中访问/调用非静态成员(如: 实例变量, 非静态方法).


    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    上面关于关键词 final 和 static 的阐述可算是前一节内容的延伸, 下面我们进入本节的正题 : 继承 ……

    4. 继承

    关于"继承"的概念、为什么要继承、继承的好处等一些基础性的知识在本Blog中的另一文章《面向对象起步 --- 封装、继承、多态》已有讲述, 因此此处不再赘述, 本文仅对如何在Java中实现继承进行阐述……

    我们沿袭前面的例子, 由 Pig 类派生出一个子类 SmallPig (小猪):
    在  com.bailey.study.animal 包内新建一个类, 并使用 extends 关键词实现继承, 代码如下:

    Java程序设计入门 (二) - bailey - Baileys Blog
     
    如上图所示, 当添加了代码 extends Pig 后Eclipse提示了一个错误, 大意是: 在父类 Pig 中未定义默认构造函数 Pig(), 因此必须定义一个明确的构造函数.
    还记得上节最后讲述构造函数的时候我们列出了几点需要注意的, 其中第(2)点当时可能看不明白, 现在就来一起解释一下:
    继承事实上是在父类的基础上进行"扩展(extend)", 当实例化子类的时候, Java会先去实例化其父类, 因此, 若父类中没有那个不带任何参数的缺省(默认)构造函数, 那么子类在实例化的时候就必须明确要通过哪一个父类构造函数去实例化父类, 否则…… 爹都生不出来, 儿子怎么出来? 
    呵呵~ 那咋办呢? 看上图…… Eclipse其实已经给出了2个建议(quick fix), 可以直接点击, 之后Eclipse会帮你把代码写好……

    下面的代码是经人工修改后的结果:
      1: package com.bailey.study.animal;
    
      2: 
    
      3: public class SmallPig extends Pig {
    
      4: 
    
      5:     public SmallPig(String name) {
    
      6:         this(name, "雌");
    
      7:     }
    
      8:     
    
      9:     public SmallPig(String name, String sex) {
    
     10:         super(name, sex);
    
     11:     } 
     12: }
    注意, 上述两个 SmallPig 类的构造函数均必须通过某种方式去调用父类的构造函数 (第6行 / 第10行). 
    其中, 第10行通过 super 关键词直接调用了父类中定义的 Pig(String name, String sex) 构造函数. 第6行通过 this 关键词调用了当前类中第9~11行定义的构造函数 ( 间接地调用父类构造函数). 
    试着按住Ctrl键, 用鼠标点击第6行的 this …… 看到效果了吧 (点击函数名, Eclipse会跳转到实际执行的函数的声明位置, 点击变量, 则会跳转到变量声明位置)

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    上面是继承之后, 子类中必须解决的一个问题……  下面来看另一个问题: 覆盖(Override)
    显然, SmallPig 恐怕还不能吃"米饭", 得吃"奶" Java程序设计入门 (二) - bailey - Baileys Blog
    因此, 我们为 SmallPig 重新定义一下 eat 方法, 将如下代码添加到SmallPig类中:
      1: @Override                         // @Override 注解告诉编译器注意校验(下面的方法是覆盖版本)
    
      2: public void eat(String food) {
    
      3:     if (!"".equals(food)) {     // 判断食物是否是奶
    
      4:         // 不是"奶", 抛出异常
    
      5:         throw new RuntimeException("小猪只能吃奶!");  
    
      6:     }
    
      7:     super.eat(food);             // 调用父类eat方法
    
      8: }
    先试试效果吧…… 什么? 怎么试? 好吗, 最后帮你一次…… 代如下:

    Java程序设计入门 (二) - bailey - Baileys Blog
     
    ……, 复制不了…… 呵呵, 我故意的! 防止懒人偷代码! 什么都不想动手, 只是傻看的话永远学不会……

    怎么样, 试了吗? 控制台 (Console) 是不是出现了异常信息, 并列出了函数调用栈的信息:

    Java程序设计入门 (二) - bailey - Baileys Blog
     
    第一次见到这个东东, 简单解释一下…… 
    上图这段输出的大意是: 在线程 "main"中出现了异常(Exception), 这个异常是 RuntimeException (运行时异常), 后面那几个汉字不用解释了吧~
    第2行陈述了异常是在执行到哪一行抛出的 (SmallPig.java中的16行, 也就是上面代码的第5行, 鼠标点击一下带下划线的部分试试?)
    第3行表达的意思与第2行类似: 在执行到HelloJava.java中第10行出现异常……
    如果我们从下往上读, 是不是就是程序实际执行时的调用顺序~

    也许现在最吸引你注意的是异常(Exception)这个新鲜玩意, 或是上面出现的几行看不太懂的代码(第3, 5行), 但是…… 先把它放下, 我们后面还会详细介绍. 

    现在花10分钟 (Maybe 20 分钟), 设置断点, 单步跟踪程序的执行过程, 并仔细琢磨一下 p.eat("奶")p.eat("奶", true) 是如何一步步执行的?

    也许, 你还应该花点时间改变一下 Pig 类中那些属性和方法的可访问性修饰符, 看看效果……

    也许…… 你还应该在Pig类中使用一下 final 修饰符试试…… (把 final 放在类声明部分和 eat 方法声明部分试试)



    OK, 是不是越来越复杂了, 呵呵~  确保前述内容没有太大的问题之后, 继续往下看……

    虽然作为一头人品正常的Java程序设计入门 (二) - bailey - Baileys Blog, 生活就是"吃"和"睡", 这是多少人向往的生活啊……  但在下私心里估摸着, 猪儿的童年想必也是极喜欢玩耍的吧, 或许……平日间偶尔玩一下也是极好的~  Java程序设计入门 (二) - bailey - Baileys Blog 娘娘饶命, 在下已看到天上一群乌鸦飞过…… 再这个调调在下做不到……

    呵呵, 来吧, 给小猪类加个"玩"的方法…… 代码如下:
    public void play(String toy) {
    
        System.out.println(name + "玩了会" + toy);		// 把 Pig 类中的 name 属性改为 protected 修饰就不报错了
    
    }
    意思都明白吧…… 如果我们执行"小花"的play方法, 例如: p.play("球"); 那应该输出: 小花玩了会球

    OK, 继续, 修改 HelloJava.java 的代码, 变成如下样式:

    Java程序设计入门 (二) - bailey - Baileys Blog
     
    咦~ 好多错…… 别急, 我们一起来细细琢磨……
    (1) 先把第13~33行注释掉 (选中13~33行, 按 Ctrl + /  , 呵呵, 又学了一招~ 如果你不嫌麻烦, 删除也行), 运行一下试试…… 显然, 这没问题……
    (2) 把13~15行加上…… 先看13行, 有意思吧, SmallPig 可以变成 Pig…… 废话~ 小猪当然也是猪了…… 这个故事告诉我们: 子类对象可以非常顺畅地转化为父类(祖先)的实例; 再看15行, 不用运行, Eclipse已经告诉你有问题了…… 既然 SmallPig 已经变成 Pig ( p2 )了, 那自然就不再会有play方法了, 因为 Pig 中压根就没定义过play方法. 这个故事告诉我们: 子类对象变成父类(祖先)实例后, 子类中新增的属性和方法将不再可用. 严格来说是不可视, 并未真正消失.
    (3) 把15行注释掉, 加上17 ~ 19行. 嘿嘿~ 猪 p2 被"强制转换"成小猪了, 这是允许的. 并且, 第19行它又可以玩球了…… 运行试试…… 这个故事又告诉我们: (a) 类型是可以强制转换的, 语法见代码; (b) 子类对象变成父类(祖先)实例后, 新增属性和方法会暂时不可用, 但再次转回子类后, 那些属性/方法就又可用了.
    (4) 21行是一条华丽丽的分隔线…… 歇会儿~ 喘口气, 想明白了继续……
    (5) 加上23 ~ 25行, 25行出错了, 这个错误比较低级, 自己应该想得明白, 我就不侮辱你的智慧了……
    (6) 注释掉25行, 加上27~29行, 又出错! 为什么? 因为…… 父类对象不能"顺畅地"直接转换为子类的实例(与13行对比一下……). 虽然我们可以硬生生的拿着q2去play (第29行), 但27行编译都过不了, 那就别提运行了……
    (7) 注释掉27~29行, 加上31~33行…… 呵呵, "强制转换"真的很牛吧~ 第31行一切正常, 编译也是可以通过的. 但是…… 运行试试…… 会 在33行抛出异常(虽然编译没错). 这个故事告诉我们: "强制转换"真的很强, 它能让编译器闭嘴, 但是…… 猪就是猪, 它绝对不可能变成小猪, 即使变成了小猪, 也是个半残废 (不会"play").

    不要怀疑自己的智商, 彻底把你绕晕一直是我追求的目标…… OMG~ 谁扔的鸡蛋……
    Just Relax ! 慢慢想, 想不明白也不要紧, 关键是多练习, 做多了……掉坑里的次数就多了, 自然也就长大了……

     


    不知不觉中, 我们已经把面向对象三大特性 (封装、继承、多态) 最基本的实现形式学完了, 小朋友们是否学会了呢? 如果喜欢记得叫上爸爸妈妈一起来顶贴哦~


    按照惯例, 给点温馨提示: 
    (1) Java中无论是原生的类, 还是你自己定义的类, 它们都有一个共同的祖先: Object, 这是所有"类"的家谱中的"根". (如果在声明类时没有extends..., 那Java会自己帮你加上 extends Object).  自己看看Object里被预先定义了些什么方法和属性吧……
    (2) 正因为(1)中所述, 有时我们要将两个"不相关"类的对象作为参数传递给一个函数时, 该函数的参数类型可选用Object. 但这只是权宜之计, 如果你的代码中真出现这样情况, "可能"意示着那个函数设计得有问题(违背了函数功能独立性原则, 因为你把2类不相关的参数传给同一个函数处理) 
    (3) 判断两个变量是否"相等"时, 千万注意: 对于引用类型(对象), 比较运算符"=="判定的是这两个变量是否是"同一个"(在内存中就是同一个空间), 而非判定两个变量的"值"是否"相等". 同理, 运算符"!="类似……  
         若要判定"值"是否"相等", 一般使用equals方法. 当然, 对于我们自己定义的类, 必要时应覆盖Object类中已定义的equals方法, 否则, 使用equals方法判定的结果与"=="判定的结果相同. 对于非引用类型(可简单理解为Java预定义的那些类型名以小写字母开头的类型, 如: int, boolean, char, long, double……) 不存在前述问题.  可运行下面的代码, 比较一下就明白了: 
    package com.bailey.study.test;
    
    public class Test {
    
        public static void main(String[] args) {
    
    	String a = "1";
    
    	String b = new String("1");
    
    	
    
    	if (a == b) {
    
    	    System.out.println("==");
    
    	} else {
    
    	    System.out.println("!=");
    
    	}
    
    	
    
    	if (a.equals(b)) {
    
    	    System.out.println("equal");
    
    	} else {
    
    	    System.out.println("not equal");
    
    	}
    
        }
    
    }





    后面的教程中我们还要继续延伸这个"猪"的例子, 为了保证你的代码与我的例子没太大出入, 现将到目前为止的代码附上……

    文件结构:
    Java程序设计入门 (二) - bailey - Baileys Blog
     
    Pig.java
    package com.bailey.study.animal;
    
    public  class Pig {
    
        protected String name;
    
        final public String sex;
    
        
    
        public Pig(String name) {
    
    	this(name, "未知");
    
        }
    
        public Pig(String name, String sex) {
    
    	this.name = name;
    
    	this.sex = sex;
    
        }
    
        public void eat(String food) {
    
    	System.out.println(name + "吃了" + food);
    
        }
    
        public void eat(String food, boolean isHot) {
    
    	if (isHot) {
    
    	    System.out.println("等呀等... 终于凉了...");
    
    	}
    
    	eat(food);
    
        }
    
        public void setName(String name) {
    
    	this.name = name;
    
        }
    
        public String getName() {
    
    	return name;
    
        }
    
    }
    
    SmallPig.java
    package com.bailey.study.animal;
    
    public class SmallPig extends Pig {
    
        public SmallPig(String name) {
    
    	this(name, "未知");
    
        }
    
        public SmallPig(String name, String sex) {
    
    	super(name, sex);
    
        }
    
        @Override
    
        public void eat(String food) {
    
    	if (!"".equals(food)) { // 判断食物是否是奶
    
    	    throw new RuntimeException("小猪只能吃奶!"); // 不是"奶", 抛出异常
    
    	}
    
    	super.eat(food); // 调用父类eat方法
    
        }
    
        public void play(String toy) {
    
    	System.out.println(name + "玩了会" + toy);
    
        }
    
    }
    
    HelloJava.java
    package com.bailey.study.test;
    
    public class HelloJava {
    
        public static void main(String[] args) {
    
        }
    
    }
    


  • 相关阅读:
    弹出的iframe 表单无法获取焦点
    记录IE的两则BUG
    manifest 在移动端的应用实验
    [hover 状态图片变小] 淘宝的一个小用户体验
    使用 JSONP 的几点注意
    Apple在线商店iphone5, ipad mini到货提醒 邮件
    关于弹窗中的事件多次执行
    zen coding 插件使用实践
    hadoop系列整理Spark
    hadoop系列整理HBase
  • 原文地址:https://www.cnblogs.com/toge/p/6114745.html
Copyright © 2020-2023  润新知