深入理解JAVA语言

452 查看

1. 变量及其传递

基本类型变量(primitive type)和引用类型变量(reference type)

  1. 基本类型(primitive type):其值直接存于变量中。“在这里”

  2. 引用型(reference type) 的变量除占据一定的内存空间外,它所引用的对象实体(由new 创建)也要占据一定空间。“在那里”,可以理解为指针

代码

MyDate m,n;
m=new MyDate();
n=m;

m和n都指向同一个对象,两者都可以理解为一个指针。通过m和n都可以操纵同一个对象。

字段变量(Field)与局部变量(Local variable)区别

从位置看

  1. 字段变量(Field):又称成员变量(member variable),域变量,在类中;上图的latitudelongitude都是字段变量。

  2. 局部变量(Local variable):又称本地变量(local variable),在方法中定义的变量或方法的参变量。上图的argslima以及latInlonIn都是局部变量。

从内存角度看

Memory Model的建立流程如下所示

上图首先在堆中新建SimpleLocation类的对象,该对象拥有两个变量latitudelongtiude-->>

上图展示调用函数SimpleLocation(double latIn,double lonIn)期间,内存中新建了临时空间,如图中constructor's scope所示。-->>

如上所示,局部(临时)变量laiInlonIn将值传递给SimpleLocation类的对象latitudelongtiude后,则对象的建立成功。该函数调用结束后,局部变量latInlonIn以及this会在内存中消失。该构造器(constructor) 先返回对象的地址(指针)给 lima 变量,然后消失。最终,lima将指向新对象。

生命周期:Field 是在随着对象的创建,产生的在堆(The heap)中;Local variable 是随着方法的调用期间按需生成相应的内存空间。
初始值:Filed 编译器会自动赋初值,Local variable 需要显式赋值,否则编译无法通过;

从语法角度看

  1. 字段变量属于类,可以用public,private,static,final 修饰。

  2. 局部变量不能够被访问控制符及static修饰

  3. 都可以被final修饰

变量的传递(与c语言相似)

调用对象方法时,要传递参数。

  • 在传递参数时,Java 是值传递,即,是将表达式的值复制给形式参数。

  • 对于引用型变量,传递的值是引用值,而不是复制对象实体,可以改变对象的属性

方法的返回

  • 返回基本类型。

  • 返回引用类型。它就可以存取对象实体。

代码

Object getNewObject() {
Object obj=new Object();
return obj; }

调用时:

Object p= GetNewObject();

2. 多态和虚方法调用

多态(Polymorphism)

是指一个程序中相同的名字表示不同的含义的情况。

1. 编译时多态

重载(overload) (多个同名的不同方法)。

代码

p.sayHello();
p.sayHello(“Wang”);

2. 运行时多态(更重要)

  • 覆盖(override) (子类对父类方法进行覆盖)

Polymorphism means that a variable of a supertype can refer to a subtype object.

  • 动态绑定(dynamic binding) ----虚方法调用(virtual method invoking)

  • 在调用方法时,程序会正确地调用子类对象的方法。

  • 多态优点:大大提高了程序的抽象程度和简洁性。

代码

public class DynamicBindingDemo {
    public static void main(String[] args){
        m(new GraduateStudent());//传递的是子类对象,编译通过
        m(new Student());//同上!
        m(new Person());//同上!
        m(new Object());
    }
    public static void m(Object x){
        System.out.println(x.toString());
    }
}
class GraduateStudent extends Student{
}
class Student extends Person{
    public String toString(){
        return "student";
    }
}
class Person{
    public String toString(){
        return "Person";
    }
}

运行结果:

student
student
Person
java.lang.Object@60e53b93

从以上测试代码看来,虽然 m(Object x)定义的形参是Object类的,但允许实际参数传递子类对象:m(new GraduateStudent());,编译通过。
另外:在类继承关系链中,对同一个方法可能对应有多个实现,但在运行时由JVM自动搜索绑定哪个实现。动态绑定流程:从最近的类(最低,最具体)找,直到 Object 类(最高,最抽象)。只要找到了实现方法就停止。比如现在GraduateStudent类中找,没找到,就在Student类中找,结果找到了,就停止该搜索。
这里需要注意区别构造函数的执行流程。

上溯造型

  • 上溯造型(upcasting) :是把派生类型(subclass)当作基本类型(upclass)处理.

It is always possible to cast an instance of a subclass to a variable of a superclass(knowns as upcasting) .

Person p = new Student();
void fun(Person p ){...}

在这里,虽然P的声明类型是Person,但它的实际类型是Student,Student是Person的子类。在被fun(Person p)调用时,仍可以casting。这也是动态绑定的范畴。

涉及类型转换的问题:

m(new Student());
//等价于
Object o = new Student();
m(o);

现在假设,要将o分配给一个Student类的变量,该如何做?
方法1

Student b = o;//编译报错,因为在编译器看来,o是一个Object类的变量,并不一定是个Student类的变量

方法2

Student b = (Student)o;//这其实是downcasting了,把一个Object类的对象向下映射为Student类的,编译通过。但是前提是得确保o变量的真实类型是Student类的,否则会抛出ClasCastException错误。

虚方法的调用

如何实现运行时的多态?(虚方法调用)

  • 子类重载了父类方法时,运行时系统根据调用该方法的真实类型(actual type)来决定选择哪个方法调用

  • 所有的非final方法都会自动地进行动态绑定

如何确定动态类型?

  • instanceof 是 java 的关键字。

  • 用变量 instanceof 来判断一个对象的真实类型(actual type)

  • 结果是boolean 值

代码

Object myObject = new Circle();
if(myObject instanceof Circle){
    System.out.println("The circle diameter is " + ((Circle)myObject).getDiameter());
}

Object myObject = new Circle();新增一个Object类的myObject变量,但是,myObject的真实类型(actual type)是Circle()类型,所以myObject instanceof Circle 返回True

另外,为什么要进行对myObject进行downcasting,即 (Circle)myObject ?

编译时,myObject 的声明类型是Object便于编译器决定采用哪个方法,比如myObject.getDiameter()将会引起编译错误,所以要类型转换 (Circle)myObject

多说一句,为什么要将myObject 设定为 Object对象?

将一个变量声明为父类类型,是为了更好地抽象编程(generic programming),这样myObject能够接受任何子类对象

什么情况不是虚方法调用

  • Java中,普通的方法是虚方法

  • 但static,private方法不是虚方法调用

三种非虚的方法

  • static的方法(从名字看,是静态,与动态绑定相对),以声明的类型为准,与实例类型无关

  • private方法子类看不见,也不会被虚化

  • final方法子类不能覆盖,不存在虚化问题

代码

public class InvokeStaticMethod {
/*调用静态方法来
*/
    public static void main(String[] args){
        Circle c = new Circle();
        Shape s = new Shape();
        Shape d = new Circle();
        
        doSomething(c);
        doSomething(s);
        doSomething(d);

        doSomethingVer2(c);
        doSomethingVer2((Circle)d);//必须强制类型转换为Circle()
    }
    static void doSomething(Shape s){//注意该方法声明为静态,非虚调用,参量的声明类型是Shape,所以只能匹配到Shape类型的参数。
        s.draw();
    }
    static void doSomethingVer2(Circle s){//注意该方法声明为静态,形参只能匹配到Circle类型的参数;
        s.draw();
    }
}
class Shape {
    static void draw(){
        System.out.println("draw shape");
    }
}
class Circle extends Shape{
    static void draw(){
        System.out.println("draw circle");

    }
}

输出结果:

draw shape
draw shape
draw shape
draw circle
draw circle

3. 对象的构造和初始化

构造方法(constructor)

  1. 对象都有构造方法

  2. 如果没有,编译器加一个default构造方法

  3. 类的成员变量和方法是可以继承的,但是类的构造器是不能继承的。只能通过调用,又分为显式调用和隐式调用。

调用本类或父类的构造方法

  1. this调用本类的其他构造方法。

  2. super调用直接父类的构造方法

  3. this或super要放在第一条语句,且只能够有一条

  4. 如果既不是调用this,也不是调用super方法,则编译器会自动加上super(),也就是调用直接父类的无参方法。

可以看出,原则上必须令所有父类的构造方法都得到调用,否则对象的构建就不成功。

In any case, constructing an instance of a class invokes the constructors of all the superclasses along the inheritance chain. This is called constructor chaining.
--Introduction to Java Programming

构造方法的执行过程遵照以下步骤:

  1. 调用本类或父类的构造方法,直至最高一层(Object)

  2. 按照声明顺序执行字段的初始化赋值

  3. 执行构造函数中的各语句。

简单地说: 先父类构造,再本类成员赋值,最后执行构造方法中的语句。

创建对象时初始化

  1. p = new NoConstructorTest(){{ a="A"; b="B"; }};

  2. 这样可以针对没有相应构造函数但又要赋值。

  3. 注意双括号。

代码

public class NoConstructorTest {
    String a;
    String b;
    //no constructors
    public static void main(String[] args){
        NoConstructorTest p = new NoConstructorTest(){{ a="A"; b="B"; }};
        System.out.println("the instance can be given value without defining constructor.\na is:\n"+p.a+"\nb is: \n"+p.b);
    }
}

输出结果

the instance can be given value without defining constructor.
a is:
A
b is: 
B

可见,该对象的初始化是成功的。

4. 对象清除与垃圾回收

迟点准备两个例子作为解析

5. 内部类与匿名类

  1. 内部类( inner class )是在所在类中的特殊成员

  2. 匿名类( anonymous class)是一种特殊的内部类,它没有类名。

内部类(inner class)

  1. 内部类是所在类的成员。

  2. 编译器生成xxxx$xxxx这样的class文件

  3. 内部类不能够与外部类同名

  4. 优点是:在某些程序中,使用内部类来简化程序(比如减少source file的个数);内部类可以引用所在外部类的属性和方法。

内部类的使用

  • 在封装它的类的内部使用内部类,与普通类的使用方式相同。

  • 在其他地方使用类名前要冠以外部类的名字。在用new创建内部类实例时,也要在 new 前面冠以对象变量,可以用 外部对象名.new 内部类名(参数)

在内部类中使用外部类的成员

  • 内部类中可以直接访问外部类的字段及方法。即使private也可以,因为内部类本质上也是一个类成员。

  • 如果内部类中有与外部类同名的字段或方法,则可以用 外部类名.this.字段及方法

代码

class A {
    private int s=3;
    public class B{
        private int s=2;
        public void mb(int s){
            System.out.println(s);
            System.out.println(this.s);
            System.out.println(A.this.s);
        }
    }
}

内部类的修饰符

内部类与类中的字段、方法一样是外部类的成员,它的前面也可以有 访问控制符和其他修饰符。

  1. 访问控制符:public , protected,默认及private。 注:外部类只能够使用public修饰或者默认

  2. final , abstract

static 修饰符

  • static修饰的内部类,实际上是一种外部类。

  • 因为它与外部类的实例无关

static类的使用

  1. 实例化static类时,在 new前面不需要用对象实例变量;

  2. static类中不能访问其外部类的非static的字段及方法,既只能够访问static成员。

  3. static方法中不能访问非static的域及方法,也不能够不带前缀地new 一个非
    static的内部类。

局部类

在一个方法中也可以定义类,这种类称为”方法中的内部类” ,或者叫局部类(local class)

局部类的使用

  1. 同局部变量一样,方法中的内部类,不能够被publicprivateprotectedstatic 修饰, 但可以被 final 或者 abstract 修饰。

  2. 可以访问其外部类的成员

  3. 不能够访问该方法的局部变量,除非是final局部变量

匿名类(anonymous class)

匿名类( anonymous class)是一种特殊的内部类

  • 它没有类名,在定义类的同时就生成该对象的一个实例

  • “一次性使用”的类

匿名类的使用

  1. 不取名字,直接用其父类或接口的名字。

    • 也就是说,该类是父类的子类,或者实现了一个接口

    • 编译器生成 xxxxx$1之类的名字,其中1表示第一个匿名类。

  2. 类的定义的同时就创建实例,即类的定义前面有一个new

    • new 类名或接口名(){......}

    • 不使用关键词class,也不使用extendsimplements

  3. 在构造对象时使用父类构造方法

    • 不能够定义构造方法,因为它没有名字

    • 如果new对象时,要带参数,则使用父类的构造方法

代码

        import javafx.application.Application;
        import javafx.event.ActionEvent;
        import javafx.event.EventHandler;
        import javafx.geometry.Pos;
        import javafx.scene.Scene;
        import javafx.scene.control.Button;
        import javafx.scene.layout.BorderPane;
        import javafx.scene.layout.HBox;
        import javafx.scene.layout.Pane;
        import javafx.scene.text.Text;
        import javafx.stage.Stage;

public class AnonymousHandlerDemo extends Application {
    @Override // Override the start method in the Application class
    public void start(Stage primaryStage) {
        Text text = new Text(40, 40, "Programming is fun");
        Pane pane = new Pane(text);

        // Hold four buttons in an HBox
        Button btUp = new Button("Up");
        Button btDown = new Button("Down");
        Button btLeft = new Button("Left");
        
        HBox hBox = new HBox(btUp, btDown , btLeft);
        hBox.setSpacing(10);
        hBox.setAlignment(Pos.CENTER);

        BorderPane borderPane = new BorderPane(pane);
        borderPane.setBottom(hBox);

        //在方法中定义的内部类称为 局部类(local class)
        //可以引用方法中的变量, 比如text
        class EnableEventHandler
                implements EventHandler<ActionEvent>{
            public void handle(ActionEvent e) {
                text.setY(text.getY() > 10 ? text.getY() - 5 : 10);
            }
        }
        //注意:必须先定义了类,才能使用.
        //否则编译器会找不到这个类而报错.
        btUp.setOnAction(
                new EnableEventHandler());

        //对比上面,非匿名类
        //下面开始使用匿名类
        btDown.setOnAction(new EventHandler<ActionEvent>() {
            @Override // Override the handle method
            public void handle(ActionEvent e) {
                text.setY(text.getY() < pane.getHeight() ?
                        text.getY() + 5 : pane.getHeight());
            }
        });
        
        //可见,使用匿名类的好处是:精简了先定义类,后使用类这一过程.
        //匿名类的使用是「一次性」的。
        
        //进一步简化,采用Lambda表达式
        btLeft.setOnAction(e -> {
            //但得遵循SAM原则
            text.setX(text.getX() > 0 ? text.getX() - 5 : 0);
        });
        
        // Create a scene and place it in the stage
        Scene scene = new Scene(borderPane, 400, 350);
        primaryStage.setTitle("AnonymousHandlerDemo"); // Set title
        primaryStage.setScene(scene); // Place the scene in the stage
        primaryStage.show(); // Display the stage
    }
    /**
     * The main method is only needed for the IDE with limited
     * JavaFX support. Not needed for running from the command line.
     */
    public static void main(String[] args) {
        launch(args);
    }
}

运行截图

6. Lambda 表达式

基本语法

Lambda表达式是从Java8增加的新语法

  • Lambda表达式(λ expression)的基本写法

    • (参数)->结果

    • 比如:(String s) -> s.length()将会返回s的长度

    • x->x*x将会返回x*x的运算结果,参数的类型都省略了。

  • 大体上相当于其他语言的“匿名函数”或“函数指针” 。

  • 在Java中它实际上是“ 匿名类的一个实例”,即是定义后马上使用,更加简洁和高效。

代码

例子一

A dolt = new A(){
    public void run(){
        System.out.println("OK");
    }
}

写成Lamdba表达式:

() -> { System.out.println(“OK”); }

例子二

A dolt = new A(){
    public double run(double x){
        return Math.sin(x);
    }
}

以上写成Lambda表达式:

(x) -> Math.sin(x);

例子三

btn.addActionListener( e -> ... } ) );//编译器能够自动识别e为ActionEvent类。

以上三个例子说明:Lambda表达式是接口或者说是接口函数的简写,基本写法是 (参数)->结果,这里,参数是()(1个参数)(多个参数),结果是指 表达式语句{语句}

能写成Lambda的接口的条件

由于Lambda只能表示一个函数,所以

  • 能写成Lambda的接口要求包含且最多只能有一个抽象函数

  • 这样的接口可以用注记(但不强求)@FunctionalInterface来表示。称为函数式接口,用于对编译器的提示:

    @FunctionalInterface
    interface A { double A( double x );}

代码

Comparator<Person> compareAge = (p1, p2) -> p1.age-p2.age;
Arrays.sort(people, compareAge);

Lambda表达式,不仅仅是简写了代码, 更重要的是:它将代码也当成数据来处理。

7. 装箱、枚举、注解

从JDK1.5起,增加了一些新的语法
大部分是编译器自动翻译的,称为Complier sugar

基本类型的包装类(wrapper class)

基本类型并不是对象,但通过使用Java API的包装类能够把基本类型包装成对象,从而方便某些方法的调用需求。

  1. 它将基本类型(primitive type) 包装成Object(引用类型)

  2. Java的八种包装类(wrapper class)如下:
    Boolean, Byte, Short, Character, Integer, Long, Float, Double

注:包装类的名称首字母亦是大写。

  • 装箱(Boxing)

    Integer I = new Integer(10);

    或简写为

    Integer I = 10;//编译器自动将基本类型包装为`Integer`对象
  • 拆箱(Unboxing)

    int i = I;
  • 上述过程,被编译器译为:

Integer I= Integer.valueOf(10);
int i = I.intValue();

枚举

枚举(enum)是一种特殊的class类型

  • 在简单的情况下,用法与其他语言的enum相似

    enum Light { Red, Yellow, Green };
    Light light = Light.Red;
  • 但实际上,编译后,它生成了 class Light extendsjava.lang.Enum,所以可以在enum定义体中,添加字段、方法、构造方法,可以当做一般的class,更加灵活。

代码

enum Direction{
    EAST("东",1), SOUTH("南",2),
    WEST("西",3), NORTH("北",4);
    private Direction(String desc, int num){//允许添加构造方法
    this.desc=desc; this.num=num; 
    }
    private String desc;
    private int num;
    public String getDesc(){ //允许添加一般方法
    return desc; 
    } 
    public int getNum(){ //允许添加一般方法
    return num; 
    }
}

注解(annotation)

又称为注记、标记、标注、注释(不同于comments)
是在各种语法要素上加上附加信息,以供编译器或其他程序使用
所有的注解都是 java.lang.annotation.Annotation 的子类
常用的注解,如

  • @Override

  • @Deprecated 表示过时的方法

  • @SuppressWarnings 表示让编译器不产生警告

  • 自定义注解,这个很少见啦。

    public @interface Author {
        String name();
    }

8. 没有指针的 JAVA 语言

引用(reference)实质就是指针(pointer),但在Java中,引用是安全的指针

Java标榜其中对C/C++一个很大的改进就是:Java对程序员屏蔽了变量地址的概念,减少指针误用。

比如:

  • 会检查空指引

  • 没有指针运算 *(p+5)

  • 不能访问没有引用到的内存

  • 自动回收垃圾

C语言指针在Java中的体现

1. 传地址 -> 对象

Java引用类型(reference type),引用本身就相当于指针,可以用来修改对象的属性、调用对象的方法。
如交换两个整数,在C语言中:

void swap(int x, int y){ 
    int t=x; 
    x=y;
    y=t; 
} 
int a=8, b=9; 
swap(&a,&b);

在Java中,无法实现该交换。但可以使用一种变通的办法,传出一个有两个分量x,y的对象。
变通办法,但显得很笨:

class Test{
    public static void swap2(final int [] arr, final int pos1, final int pos2){
        final int temp = arr[pos1];
        arr[pos1] = arr[pos2];
        arr[pos2] = temp;
    }
    public static void main(String [] args){
        int [] a ={1,2};
        swap2(a,0,1);
        System.out.println(a[0]+" "+a[1]);
    }
}

运行结果:

2 1

2. 指针运算 -> 数组

在C中的指针*(p+5) ,在Java中则可以用 args[5]

3. 函数指针 -> 接口、Lambda表达式

例如上述:求积分、线程、回调函数、事件处理。

4. 指向结点的指针 -> 对象的引用

在链表中有该类:

class Node {
    Object data;
    Node next; 
}

next就是一个指向对象的指针。

5. 使用JNI

Java Native Interface(JNI) ,它允许Java代码和其他语言写的代码进行交互。