03-面向对象编程

面向对象编程

问:面向对象的基本特征

(1) 封装:利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过其对外已授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外提供的接口来访问该对象。

(2) 继承:继承是使用已存在的类作为基础定义新类,新类的定义可以增加新的数据或新的操作,也可以复用父类的操作,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

(3) 多态:多态就是指程序中定义的引用变量所指向的对象的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

问:类

例1

问:new关键字

例1

问:访问控制符

Java类的成员没有写任何访问修饰符时,表示friendly访问权限,后面写作default。


图 1

访问权限大小:public>protected>default>private

注意:

  • 对于有继承关系的两个类,例如class B extends A,如果A和B在不同的包中,那么子类不能通过父类的实例访问父类的protected成员,但可以直接使用从父类继承的protected成员

例1:

父类:

package p1;

public class Father{
    public int field1=1;
    protected int field2=2;
    int field3=3;
    private int field4=4;
}

子类:

package p2;

import p1.Father;

public class Child extends Father{ 
    public void fun() {
        System.out.println(field1);
        System.out.println(field2);
        System.out.println(field3); // 错误
        System.out.println(field4); // 错误

        Father f = new Father();
        System.out.println(f.field1);
        System.out.println(f.field2); // 错误:不同包下的子类不能通过父类实例访问protected成员
        System.out.println(f.field3); // 错误
        System.out.println(f.field4); // 错误
    }
}

如果将Child类也放在p1包中,则:

package p1;

import p1.Father;

public class Child extends Father{ 
    public void fun() {
        System.out.println(field1);
        System.out.println(field2);
        System.out.println(field3);
        System.out.println(field4); // 错误

        Father f = new Father();
        System.out.println(f.field1);
        System.out.println(f.field2);
        System.out.println(f.field3);
        System.out.println(f.field4); // 错误
    }
}
  • Java中,外部类的访问修饰符只能是public或default,类成员(包括内部类)的修饰符可以是以上四种。

例2:分析下面代码的运行结果

class Person{
    private String name = "Person";
    int age=0;
}
public class Child extends Person{
    public String grade;
    public static void main(String[] args){
        Person p = new Child();
        System.out.println(p.name);
    }
}

答案:程序编译错误。子类无法访问父类的私有成员。

例2:Java代码查错

public class Something {
    void doSomething() {
        private String s = "";
        int l = s.length();
    }
}

局部变量前不能放置任何访问修饰符(private,public和protected)。final可以用来修饰局部变量

问:Java中,什么是构造方法?什么是构造方法重载?什么是复制构造方法?

Java中每一个类都有构造方法,当使用关键字new实例化一个对象时,类的构造方法就会被自动调用,完成对象的初始化工作。

构造方法是一种特殊的方法,具有以下特点:

(1) 构造方法的方法名必须与类名相同;

(2) 构造方法没有返回值类型,也不能定义为void;

(3) 构造方法不能被static、final、synchronized、abstract、native等关键字修饰,但可以被public、private、protected修饰

(4) 一个类可以定义多个构造方法,如果在定义类时没有定义构造方法,则编译系统会自动插入一个无参数的默认构造方法,这个构造器不执行任何代码;

(5) 构造方法可以被重载,以参数的个数、类型、顺序进行区分;

(6) 构造方法不能被继承,因此不能被重写,子类使用父类的构造方法需要使用super关键字

例1:

Java中构造方法重载和方法重载很相似,可以为一个类创建多个构造方法,每一个构造方法必须有它自己唯一的参数列表。

Java不支持像C++中那样的复制构造方法,但是这并不代表Java中没有这种机制,Java中Object类的clone()方法就是这种机制的体现。

问:static关键字

“static”关键字可以用于修饰类的成员变量和成员方法,被其修饰的成员变量被称为类变量,被其修饰的成员方法称为类方法,它们随着类的加载而加载,使得这些变量和方法可以在类没有被实例化的情况下直接通过类名进行访问。

(1) 被static修饰的成员方法只能访问static成员,不可以访问非static成员

Java中,非static成员是与对象关联在一起的,必须创建一个对象后,才可以访问该对象的方法或属性;而被static修饰的变量和方法是属于类的,它们随着类的加载而加载,同时对static变量进行初始化并执行static代码块。如果static环境中的代码尝试访问非static的变量,编译器会报错,因为类的加载先于变量的创建,类加载的时候这些变量还没有被创建出来,例如,在static方法中访问某对象的非static方法,此时尚不能保证对象已经被实例化。因此在static环境中不能访问非static变量。

例1

package NowCoder;
class Test {
    public static void hello() {
        System.out.println("hello");
    }
}
public class MyApplication {
    public static void main(String[] args) {
        Test test=null;
        test.hello();
    }
}

上述代码:能编译通过,并正确运行

(2) Java中静态变量只能在类主体中定义,不能在方法中定义。

静态变量属于类所有而不属于方法所有。静态变量是在方法之前被加载的,在static加载时,对象还没有创建,方法也还没有分配空间。所以static变量只能是类成员变量,而不能是局部变量。

例1:分析下面代码的执行结果。

public class Test {
    public int aMethod() {
        static int i = 0;
        i++;
        return i;
    }
    public static void main (String args[]) {
        Test test = new Test();
        test.aMethod();
        int j = test.aMethod();
        System.out.println(j);
    }
}

答案:程序编译失败

问:局部变量、静态变量和实例变量

Java中主要存在如下几种类型的变量:

  • 局部变量
  • 静态变量(类变量):属于类,被static修饰符修饰的变量,也称为类变量,它属于类、而不属于类的任何一个对象。一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。静态变量可以让多个对象实现共享内存。
  • 实例变量(成员变量/非静态变量):属于对象,必须依存于某一实例,即需要先创建对象然后才能通过对象访问到它。

例1:分析下面代码的执行结果。

public class Main {
    static {
        int x = 5;
    }
    static int x, y;

    public static void main(String args[]) {
        x--;
        myMethod();
        System.out.println(x + y + ++x);
    }

    public static void myMethod() {
        y = x++ + ++x;
    }
}

A、compiletime error    B、prints:1    C、prints:2    D、prints:3    E、prints:7   F、prints:8

答案:D

注意静态代码块中的语句声明,重新定义了一个局部变量int x = 5,执行完静态代码块后,局部变量就被销毁。
此时全局变量x 还是默认值0,执行到打印语句时,x=1,y=0,对于打印语句System.out.println(x+y+ ++x)中的x+y+ ++x由于+是左结合的,因此x+y+ ++x,即1+0+(++x)=3.

问:继承

继承时类的实例化问题:在实例化一个类时,一定是先创建其父类对象,即先执行父类的构造函数,然后再创建当前类对象。如果子类没有显示地调用父类的构造函数,编译器会自动加入父类的无参的构造函数super()(这也要求父类要有无参数的构造函数,否咋将出现编译出错)。

例1:

package test; 
class FatherClass { 
    public FatherClass() { 
        System.out.println("FatherClass Create"); 
    } 
} 

class ChildClass extends FatherClass { 
    public ChildClass() { 
        System.out.println("ChildClass Create"); 
    } 
    public static void main(String[] args) { 
        FatherClass fc = new FatherClass(); 
        ChildClass cc = new ChildClass(); 
    } 
} 

程序的输出结果如下:
FatherClass Create 
FatherClass Create 
ChildClass Create

例2

问:Java支持多继承么?

Java中的类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是Java中的接口支持多继承,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。

问:super和this

this代表当前类对象,super代表当前类对象的父类对象。

在子类构造方法中,使用super()调用的是其父类的构造方法,而使用this()调用的是同一个类中重载的构造方法

super关键字

在Java中super指代父类对象,通过它可以访问父类对象中非private修饰的成员变量和方法。

(1) Java中子类不能继承父类的构造方法,只能调用父类构造方法;

(2) 子类的构造方法可以显示地使用super调用父类构造方法;

(3) 当子类构造方法中没有显示地使用super来调用父类构造方法时,系统会默认采用super()调用父类的无参构造方法,这时父类必须提供无参构造方法,否则编译出错。

例1:下面程序的输出结果是多少?

class A {
    public String name = "llyA";

    public void getName() {
        System.out.println("父类getName->" + name);
    }
}

class B extends A {
    public String name = "llyB";

    @Override
    public void getName() {
        System.out.println("子类getName->" + name);
        super.getName();
    }
}

public class Main {
    public static void main(String[] args) {
        B b = new B();
        b.getName();
    }
}
// 输出:
子类getName->llyB
父类getName->llyA

例2例3例4例5

例6:下面程序的输出结果是多少?

import java.util.Date;

public class Test extends Date{
    public static void main(String[] args) {
        new Test().test();
    }
    public void test(){
        System.out.println(super.getClass().getName());
    }
}

在test方法中其实在调用从父类继承的getClass()方法
输出为: Test
如果想得到父类的名称,应该使用:getClass().getSuperClass().getName();

this关键字

注意:

(1) 使用super()或this()时,它们必须放在构造方法的第一行,否则编译通不过。

(2) 由于this()调用的构造函数默认调用super()方法,所以规定this()和super()不能同时出现在一个构造函数中。

(3) static环境,包括static方法和static语句块,在执行时还没有构造对象实例,因此不能在static环境中使用this和super

问:Java中的方法重写(或覆盖,Overriding)和方法重载(Overloading)是什么意思?

方法重写和方法重载都是Java多态性的不同表现(动态绑定 dynamic binding)

(1) 方法重载是一个类中多态性的一种表现,发生在同一个类里面两个或多个方法的方法名相同但是参数列表不同的情况。

  • 函数名必须相同;
  • 函数参数列表必须不相同:可以是参数个数、参数类型或者参数顺序不同;
  • 函数的返回值类型、访问权限、异常类型可以相同,也可以不相同。即不能通过返回值类型、访问权限和异常类型对方法进行重载。

例1:为什么不能通过返回值类型进行方法重载?

因为我们有时候调用一个方法时可以不定义返回结果变量,即不要关心其返回结果,例如,我们调用map.remove(key)方法时,虽然remove方法有返回值,但是我们通常都不会定义接收返回结果的变量,这时候假设该类中有两个名称和参数列表完全相同的方法,仅仅是返回类型不同,java就无法确定编程者倒底是想调用哪个方法了,因为它无法通过返回结果类型来判断。所以,重载的方法是可以改变返回值的类型;只能通过不同的参数个数、不同的参数类型或不同的参数顺序来实现重载。

(2) 方法重写是父类与子类之间多态性的一种表现,是说子类重新定义了父类的方法;方法的重写满足两同两小一大原则

  • 方法名相同,参数列表相同
  • 子类方法返回值类型小于或等于父类方法返回值类型;
  • 子类方法抛出的异常类型小于或等于父类方法抛出的异常类型 (小于:子类方法抛出的异常时父类方法抛出异常的子类);
  • 子类方法访问权限大于或等于父类方法访问权限 (public>protected>defualt(默认修饰符)>private)。如果父类的方法是private访问权限,则子类并没有重写父类的方法,只是在子类中定义了一个全新的方法。

注意:Java不支持运算符重载。

例1

问:Java中是否可以覆盖(override)一个被private或static修饰的方法?

private修饰的方法不能被覆盖,因为被private修饰的父类方法在子类中是不可见的。

Java中静态方法在形式上可以被重写,即子类中可以重写父类中静态的方法。但是实际上从内存的角度上静态方法不可以被重写,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

问:final关键字

final关键字可以用于修饰类、变量和方法。

(1) final修饰的类不可被继承(因此一个类不能同时被final和abstract关键字修饰);

(2) final修饰的方法不可被重写;

(3) final修饰的变量的值不可被修改,一旦获得初始值,该变量就不能被重新赋值。

例1:

使用final关键字修饰一个引用变量时,引用变量的值不可被修改,但引用变量所指向的对象的内容是可以改变的。例如:
final StringBuffer sb=new StringBuffer("immutable");
当执行sb.append(" broken!");时,编译正确;
但是当执行sb=new StringBuffer("");时,将报告编译错误。

例2例3例4

(4) 形式参数只能用final修饰符,任何其它修饰符都会引起编译错误。但是用这个修饰符也有一定的限制,就是在方法中不能修改参数的值(但是从上面例1可以看出,参数引用的对象仍然是可以修改的)。不过在一般情况下,一个方法的形参不用final修饰。只有在特殊情况下:方法内部类,一个方法的内部类如果使用了这个方法的参数或局部变量,这个参数或局部变量应该是final。

(5) final修饰的成员变量既可以在定义时显示地初始化,也可以先声明而不初始化,这种成员变量称为blank final,此时可以在构造代码块或构造函数中对其赋初值,(必须在constructor结束之前,如果类中有多个构造方法,则每个构造方法中都需要进行一次初始化),否则使用未赋初值的final变量时编译会报错。

注意:对于非final修饰的类的成员变量(包括static和非static),如果开发者没有给其赋初值,在编译时,JVM自动会给非final修饰的成员变量赋初值,我们在类的成员方法中就可以直接使用、运算了。

(6) 当方法中的局部变量用final修饰时,不一定要在定义的时候就初始化,只需要在使用前保证初始化就可以,但必须要在开始使用这个变量前初始化。

例1:

class Foo {
    final int i;
    int j;
    public void doSomething() {
        System.out.println(++j + i);  // 编译出错,因为final成员变量在使用前没有赋初值
    }
}

例2:这一题解释尚不明确,不知道final方法在子类中继承了没有。

public class Car extends Vehicle{
    public static void main(String[] args){
        new Car().run();    //输出:Car
    }
    private final void run(){
        System.out.println("Car");
    }
}
class Vehicle{
    private final void run(){
        System.out.println("Vehicle");
    }
}

问题已解决:子类会继承父类的所有属性和方法,或者说子类拥有父类的所有属性和方法,但是子类无法直接访问父类的私有属性和私有方法。

这里由于子类无法访问父类的run()方法,因此子类并没有重写父类的run()方法,而是自己重新定义了run()方法。

(6) 被fianl修饰的变量是常量,参与运算时在编译时就计算出结果,不会自动改变类型;当2个final修饰的变量相操作时,结果会根据左边变量的类型进行转化

例1:

byte b1=1,b2=2,b3,b6,b8;
final byte b4=4,b5=6,b7;
b3=(b1+b2);  /*语句1*/
b6=b4+b5;    /*语句2*/
b8=(b1+b4);  /*语句3*/
b7=(b2+b5);  /*语句4*/
System.out.println(b3+b6);

代码片段中,存在编辑错误的语句是:语句1、语句3和语句4
语句1错误:b3=(b1+b2);自动转为int,所以正确写法为b3=(byte)(b1+b2);或者将b3定义为int;
语句2正确:b6=b4+b5;b4、b5为final类型,在编译时就已经变为b6=10,不会自动提升,计算结果任然是byte类型,然后结果的类型视左边变量类型而定,即b6可以是任意数值类型;
语句3错误:b8=(b1+b4);虽然b4不会自动提升,但b1仍会自动提升,所以结果需要强转,b8=(byte)(b1+b4);
语句4错误:b7=(b2+b5); 同上。同时注意b7是final修饰,即只可赋值一次,便不可再改变。

问:Object类

Java中Object是所有类的祖先类,Object类中包含如下方法:


图 1

例1:重写clone()方法时,通常都会有一行代码,是什么?

super.clone();
重写clone()方法时,首先要把父类中成员复制到位,然后才是复制子类的成员。

问:如何实现对象克隆

有两种方式可以实现对象克隆:

(1) 实现Cloneable接口并重写Object类中的clone()方法;

(2) 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Main {
    public static void main(String[] args) {
        try {
            Person p1 = new Person("Bob", 33, new Car("Benz", 300));
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bout);
            oos.writeObject(p1);

            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bin);
            Person p2 = (Person) ois.readObject();
            p2.getCar().setBrand("BYD");
            System.out.println("Person 1--->" + p1);//Person 1--->Person [name=Bob, age=33, car=Car [brand=Benz, maxSpeed=300]]
            System.out.println("Person 1--->" + p2);//Person 1--->Person [name=Bob, age=33, car=Car [brand=BYD, maxSpeed=300]]
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
class Person implements Serializable {
    private static final long serialVersionUID = -9102017020286042305L;

    private String name; // 姓名
    private int age; // 年龄
    private Car car; // 座驾

    public Person(String name, int age, Car car) {
        this.name = name;
        this.age = age;
        this.car = car;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
    }
}

class Car implements Serializable {
    private static final long serialVersionUID = -5713945027627603702L;

    private String brand; // 品牌
    private int maxSpeed; // 最高时速

    public Car(String brand, int maxSpeed) {
        this.brand = brand;
        this.maxSpeed = maxSpeed;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public int getMaxSpeed() {
        return maxSpeed;
    }

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    @Override
    public String toString() {
        return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
    }
}

问:equals()方法和==的区别

(1) ==运算符:比较两个变量的值是否相等,也就是两个变量所对应的内存中存储的数值是否相同,即栈中的内容是否相同。

  • 对于基本数据类型变量比较的是两个变量的值是否相等;如果是不同的基本数据类型之间进行比较,则遵循基本数据类型间运算的转换原则:
    • 如果两个操作数其中有一个是double类型,另一个操作数将会转换为double类型;
    • 否则,如果其中一个操作数是float类型,另一个操作数将会转换为float类型;
    • 否则,如果其中一个操作数是long类型,另一个操作数会转换为long类型;
    • 否则,两个操作数都转换为int类型。
  • 对于引用型变量表示的是两个变量指向的对象在堆中存储的地址是否相同,即两个变量是否指向同一个对象

例1:

public static void main(String[] args){
    int i=42;
    double d=42.0000;
    long l=42;
    float f=42.0f;
    float f2=42.00f;
    System.out.println(d==i);   //true
    System.out.println(f==i);   //true
    System.out.println(f==f2);  //true
    System.out.println(l==i);   //true
    System.out.println(l==f);   //true
    System.out.println(d==f);   //true
}

(2) equals()方法:继承自Object类,默认调用==进行比较。通常比较两个对象的内容是否相同,判断两个对象是否相等需要覆盖equals()方法和hashcode()方法。

注意:对于基本数据类型来说,使用equals方法时需要用该基本类型对应的包装类,因为equals()方法是针对对象来使用的!

例1:

String a=new String("foo");
String b=new String("foo");
System.out.println(a==b);        //false
System.out.println(a.equals(b)); //true

例2:寻找代码错误

if(username.equals("zhangsan")){
}

username可能为null,此时编译器会报告空指针异常,因此需要改为"zhangsan".equals(username)

问:finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

finalize()是Object类的一个方法,垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法。

但是很不幸的是,在Java中,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说finalize()可能永远不会被执行,显然指望它做收尾工作是靠不住的。 那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存。

问:多态的实现方式

(1) 静态的多态/编译时的多态性:方法重载

(2) 动态的多态/运行时的多态性:子类覆盖父类的方法,将子类的实例赋值给父类的引用,此时调用的是子类的方法;实现接口的实例赋值给接口的引用,此时调用的实现类的方法。

例1:判断对错

在java的多态调用中,new的是哪一个类就是调用的哪个类的方法。

答案:错误。

没看懂题目后面的评论,不知道跟重载有什么关系。目前我觉得下面例子是一个反例。

class Father {
    public static void run(){
        System.out.println("Father run");
    }
}
public class Child extends Father {
    public static void run(){
        System.out.println("Child run");
    }
    public static void main(String[] args) {
        Person p = new Child();
        p.run(); //这里调用的实际上是父类的run()方法
    }
}

问:抽象类和接口的区别

Java提供和支持创建抽象类和接口。

(1) 抽象类:含有abstract修饰符的class即为抽象类,abstract类不能创建类的实例对象。

  • 含有abstract方法的类必须定义为abstract class
  • 抽象类中的抽象方法,需要由子类实现,如果子类不实现所有抽象方法,则子类也需要定义为抽象类;
  • 抽象方法既不能是static的,也不能是native的,还不能是synchronized的
  • 构造方法和静态方法不可以为抽象的

(2) 接口:接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。

  • 接口中的方法默认定义为public abstract类型,接口中的成员变量默认为public static final
  • 接口中定义的方法都需要由实现类来实现,如果实现类不能实现接口中的所有方法,则实现类需要定义为抽象类

他们的共同点在于:

  • 抽象类和接口都不可以被实例化
  • 类可以不实现抽象类和接口中声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的

它们的不同点在于:从设计层面来说,抽象是对类的抽象,是一种模板设计;接口是行为的抽象,是一种行为的规范。

  • 抽象类中可以有构造方法,其作用是初始化抽象类的成员;接口中不能有构造方法
  • 抽象类中可以有普通成员变量;而接口中没有普通成员变量,只能有静态成员变量
  • 抽象类中可以包含静态方法;而接口中不能包含静态方法(Java8开始接口可以有静态方法)
  • 抽象类中可以同时包含抽象方法和非抽象方法,也可以没有抽象方法,但如果一个类中有一个抽象方法,那么当前类一定是抽象类;而接口中只有方法的声明、没有方法体,即接口中的所有方法必须都是抽象的,不能有非抽象的普通方法(Java8中接口可以有非抽象的default方法)
  • 抽象类和接口中都可以包含静态成员变量。抽象类中静态成员变量的访问类型可以任意;而接口中声明的成员变量默认为public static final类型,且只能是public static final类型
  • 抽象类中的普通成员方法的访问类型可以为public、protected和private,抽象方法必须为public或protected;而接口中的成员方法的访问类型默认为public abstract类型,且只能是public abstract
  • 一个类最多只能继承一个类,但一个类可以实现多个接口
  • 抽象类可以implements接口,抽象类可以继承具体类也可以继承抽象类;接口可以继承接口,并且接口可以实现多继承(一个接口可以继承多个接口)

例1例2例3例4

例5:Java代码查错

abstract class Something { 
    private abstract String doSomething(); 
}

编译错误:abstract修饰的方法需要由子类实现,不能用private修饰。
同理,abstract方法前不能加final。

例6:Java代码查错

interface A {
    int x = 0;
}

class B {
    int x = 1;
}

class C extends B implements A {
    public void fun() {
        System.out.println(x);
    }

    public static void main(String[] args) {
        new C().fun();
    }
}

编译错误:未明确的x调用

例7:

class Toy{
}

interface Rollable {
    Toy toy = new Toy();

    void play();
}

class Ball implements Rollable {
    public void play() {
        toy = new Toy();
    }
}

编译错误:任何在interface里声明的interface variable默认为public static final,不能被重新赋值。

例8:

接口可以继承接口;抽象类可以实现接口;抽象类可以继承具体类;抽象类中可以有静态的main方法。

例9:JDK1.8中允许接口定义默认非抽象的方法

Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫做扩展方法。

interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

Formula接口在定义calculate方法的同时还定义了sqrt方法,实现Formula接口的子类只需要实现calculate方法,默认方法sqrt将在子类中可以直接使用。

Formula formula = new Formula() {  
    @Override  
    public double calculate(int a) {  
        return sqrt(a * 100);  
    }  
};  
formula.calculate(100);     // 100.0  
formula.sqrt(16);           // 4.0 

问:instanceof关键字

instanceof前一个参数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是一个接口)。它用于判断前面的变量引用的对象是否是后面的类(也可以是一个接口)或者其子类的实例。

例1:

public static void main(String args[]) {
    List  Listlist1 = new ArrayList();
    Listlist1.add(0);
    List Listlist2 = Listlist1;
    System.out.println(Listlist1.get(0) instanceof Integer);
    System.out.println(Listlist2.get(0) instanceof Integer);
}

上面代码将输出:true true
Collection类型的集合(ArrayList,LinkedList)只能装入对象类型的数据
该题中需要装入0,是一个基本类型,但是JDK5以后提供了自动装箱与自动拆箱,所以int类型自动装箱变为了Integer类型。
List没有使用泛型,因此使用get(0)取出的元素的编译类型是Object型的,但运行时类型是Integer,所以打印true,这里体现了多态的应用。
而Listlist1把引用赋给了Listlist2,说明两个指向同一个对象,因此第二个打印的也是true。

问:内部类

内部类就是在一个类的内部定义的类。

  • 内部类中不能定义静态成员
  • 内部类可以直接访问外部类中的成员
  • 内部类可以定义在外部类的方法外面,也可以定义在外部类的方法体中

例1:内部类中可以访问外部类的成员?有没有什么限制?

如果内部类不是静态嵌套类,则内部类可以直接访问外部类的成员;
但是如果把静态嵌套类当作内部类的一种特例,则它不可以直接访问外部类的普通成员,只能访问外部类中的静态成员。

例2:内部类中是否一定不可以定义静态成员?

同样的,静态嵌套类可以定义静态成员

内部类分类,来自例2中评论部分BlueFish的总结。

(1) 成员内部类

public class Outer{
    private String name="Outer";
    private int age=99;

    public static void main(String[] args){
        Outer outer=new Outer();
        Inner inner=outer.new Inner();
        inner.show();
    }

    private class Inner{
        private String name="Inner";
        private final int num=10;
        public void show(){
            System.out.println(Outer.this.name);
            System.out.println(this.name);
            System.out.println(age);
        }
    }
}
  • Inner类定义在Outer类的内部(相当于Outer类的一个成员变量的位置处),Inner类可以使用任意访问控制符,如public、protected、private或default等,跟成员变量有4种访问权限一样;
  • Inner 类中定义的show()方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问Outer类中的私有属性age;
  • 定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去new一个内部类对象,即:内部类 对象名=外部类对象.new 内部类();
  • 编译上面的程序后,会发现产生了两个.class文件: Outer.class和Outer$Inner.class
  • 成员内部类中不能存在任何static的变量和方法,但可以定义常量:
    • 因为非静态内部类的存在是依赖于外部类的实例的,而静态变量和方法是不依赖于对象的,仅与类相关。而在加载静态域时,根本没有外部类实例,所在在非静态内部类中不能定义静态变量或方法,编译不通过;非静态内部类的作用域是实例级别
    • 常量是在编译器时确定的,放到所谓的常量池了,因此可以定义常量。

注意:

  • 外部类是不能直接使用内部类的成员和方法的,可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法;
  • 如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用this关键字,如:Outer.this.name

(2) 方法内部类,访问仅限于方法内或者该作用域内

public class Outer {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.print();
    }

    public void print() {
        final int a = 25;
        class Inner {
            private int c = 2;

            public void show() {
                System.out.println(a);
                System.out.println(c);
            }
        }
        Inner inner = new Inner();
        inner.show();
    }
}
  • 方法内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的;但这种内部类的前面可以使用final或abstract修饰符。
  • 方法内部类对其他外部类是不可见的
  • 方法内部类不能修改方法中定义的局部变量(解释看原评论,原评论这部分,我测试有点问题,大概原因是内部类访问方法局部变量时会拷贝局部变量,对局部变量修改会造成拷贝的局部变量值与方法中局部变量值不一致,为了保持局部变量值的一致性,可以将被内部类访问的局部变量声明为final)

(3) 匿名内部类:定义某一接口的实现类或定义某一类的子类的同时,还创建了该实现类或子类的实例对象,且没有为该实现类或子类定义名称

public class Outer{
    public static void main(String[] args){
        Outer outer=new Outer();
        InnerClass inner=outer.getInnerClass(2, "inner");
        System.out.println(inner.getNumber());
    }

    public InnerClass getInnerClass(int num, String str){
        return new InnerClass(){
            int count;
            int number=num+1;

            {
                count=100;
            }

            @Override
            public int getNumber() {
                return number;
            }
        };
    }

    private interface InnerClass{
        int getNumber();
    }
}
  • 匿名内部类是直接使用 new 来生成一个对象的引用;
  • 对于匿名内部类的使用,它是存在一个缺陷的,就是它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用;
  • 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,即只能继承一个类或者实现一个接口;
  • 匿名内部类中是不能定义构造函数的,匿名内部类中不能存在任何的静态成员变量和静态方法;
  • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法;
  • 匿名内部类初始化:使用构造代码块! 利用构造代码块能够达到为匿名内部类创建一个构造器的效果

内部类的作用

  • 隐藏内部操作细节
  • 解决多重继承的问题

第二点作用是使用内部类最吸引人的原因,它的存在使得Java的继承机制更加完善。众所周知,Java中的类只能继承一个类,它的多重继承在我们没有学习内部类之前是用接口来实现的,但是使用接口会存在很多不方便的地方,比如我们实现一个接口就必须实现它里面的所有方法;而有了内部类情况就不一样了,它可以让我们的类间接地继承多个抽象类或具体类:我们可以在外部类中定义多个内部类,每个内部类都可以独立地继承一个抽象类或具体类,然后再外部类中创建内部类的对象,并使用内部类的方法,这样就可以变相地实现了多继承。

例如我们有两个类Class1和Class2:

class Class1{
    public void fun1(){
        System.out.println("Class1 fun1()");
    }
}
class Class2{
    public void fun2(){
        System.out.println("Class2 fun2()");
    }
}

如果我们想要类Test既有Class1的特性,又有Class2的特性,就必须让Test类既继承Class1,又继承Class2,可是这在Java的单继承使得这种方式不可实现。那又怎么办呢?我们可以在Test中实现两个内部类InnerClass1和InnerClass2,让他们分别继承Class1和Class2,然后在Test中创建InnerClass1和InnerClass2的对象,通过这些对象调用Class1和Class2的方法,间接实现多继承。

public class Test {
    public void fun1(){
        new InnerClass1().fun1();
    }
    public void fun2(){
        new InnerClass2().fun2();
    }

    class InnerClass1 extends Class1{
    }
    class InnerClass2 extends Class2{
    }
}

使用内部类还能够为我们带来如下特性:

  • 内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立;
  • 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类;
  • 创建内部类对象的时刻并不依赖于外围类对象的创建;
  • 内部类并没有令人迷惑的”is-a”关系,他就是一个独立的实体;
  • 内部类提供了更好的封装,除了该外围类,其他类都不能访问。

说实话,上面的特性没读懂,为了保持内容完整性,姑且放在这里吧。

例1: 重要

非静态内部类是属于对象的,所以初始化时需要先初始化一个外部类实例对象,然后使用此对象调用内部类的构造方法。静态内部类属于类本身,初始化时直接使用外部类调用静态内部类的构造方法即可

public class Test{
    public static void main(String[] args){
        EnclosingOne.InsideOne obj1=new EnclosingOne().new InsideOne(); //非静态内部类
        EnclosingOne.InsideTwo obj2=new EnclosingOne.InsideTwo();       //静态内部类
    }
}
class EnclosingOne {
    //非静态内部类
    public class InsideOne {
    }
    //静态内部类
    public static class InsideTwo{
    }
}

例2:往OuterClass类的代码段中插入内部类声明,哪一个是错误的?

public class OuterClass{
    private float f=1.0f;
    //插入代码到这里
}

A.    class InnerClass{
        public static float func(){
            return f;
        }
    }
B.    abstract class InnerClass{
        public abstract float func(){}
    }
C.    static class InnerClass{
        protected static float func(){
            return f;
        }
    }
D.    public class InnerClass{
        static float func(){
            return f;
        }
    }

答案:ABCD

例3

例4:代码检查

class Outer {
    class Inner {}

    public static void foo() { 
        new Inner(); 
    }

    public void bar() { 
        new Inner(); 
    }

    public static void main(String[] args) {
        new Inner();
    }
}

编译错误:Java中非静态内部类对象的创建要依赖其外部类对象,
上面的面试题中foo和main方法都是静态方法,静态方法中没有this,也就是说没有所谓的外部类对象,因此无法创建内部类对象。
如果要在静态方法中创建内部类对象,可以这样做:new Outer().new Inner();

问:Static Nested Class和Inner Class的不同?

在方法外部定义的内部类前面加上static关键字,成为static Nested Class,它不再具备内部类的特性,侠义上讲,它不是内部类。它与普通类在运行时的行为和功能没有什么区别,只是编程引用时的语法有一些区别。

public class Outer{
    private static String name="Outer";
    private static String tag="tag";
    private int age=99;

    public static void main(String[] args){
        Inner inner=new Inner();
        inner.show();
    }

    private static class Inner{
        private String name="Inner";
        public void show(){
            System.out.println(Outer.name);
            System.out.println(this.name);
            System.out.println(tag);
            System.out.println(new Outer().age);
        }
    }
}
  • 静态内部类不能直接访问外部类的非静态成员,但可以通过new 外部类().成员的方式访问
  • 如果外部类的静态成员与内部类的成员名称相同,可通过类名.静态成员的方式访问外部类的静态成员;
  • 如果外部类的静态成员与内部类的成员名称不相同,则可通过成员名直接调用外部类的静态成员;
  • 创建静态内部类的对象时,不需要外部类的对象,可以直接创建内部类 对象名=new 内部类();

(1) Static Nested Class可以定义为public、protected、default和private

(2) Static Nested Class可以不依赖于外部类实例被实例化,而通常的内部类需要在外部类实例化后才能实例化。

  • 在外部类的外面引用Static Nested Class的方式为”外部类名.内部类名”,即在外部类的外面不需要创建外部类的实例对象,就可以直接创建Static Nested Class的实例。例如,假设Inner是定义在Outer类中的Static Nested Class,那么可以使用如下语句创建Inner类的实例:Outer.Inner inner = new Outer.Inner();
  • static Nested Class能访问外部类的非static成员变量(不能直接访问,需要创建外部类实例才能访问非静态变量)。

(3) 当在外部类中访问Static Nested Class时,可以直接使用Static Nested Class的名字,而不需要加上外部类的名字了,在Static Nested Class中也可以直接引用外部类的static的成员变量,不需要加上外部类的名字。

(4) 在静态方法中定义的内部类也是Static Nested Class,这时候不能在类前面加static关键字,静态方法中的Static Nested Class与普通方法中的内部类的应用方式很相似,它除了可以直接访问外部类中的static的成员变量,还可以访问静态方法中的局部变量,但是,该局部变量前必须加final修饰符。

问:Lambda表达式

在老版本的Java中,排列字符串一般按如下方式处理:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");  
Collections.sort(names, new Comparator<String>() {  
    @Override  
    public int compare(String a, String b) {  
        return b.compareTo(a);  
    }  
});

我们需要给静态方法Collections.sort()传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。

在Java 8中我们就没必要使用这种传统的匿名对象的方式了,Java 8提供了更简洁的语法:lambda表达式。使用lambda表达式的写法如下:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,因此上面代码可以重写成如下:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

Java编译器可以自动推导出参数类型,代码可以进一步简写成如下:

Collections.sort(names, (a, b) -> b.compareTo(a));

问:JNI(Java Native Interface) (没怎么遇到过,暂时保留)

Java的不足除了体现在运行速度上要比传统的C++慢许多之外,还体现在Java无法直接访问底层操作系统(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。

native是方法修饰符,native方法是由另外一种语言(如C/C++,汇编等)实现的本地方法,因为在外部实现了方法,所以在java代码中,就不需要声明了,有点类似于接口中的抽象方法。其实现步骤为:

  • 在Java中声明native()方法,然后编译;
  • 用javah产生一个.h文件;
  • 写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
  • 将第三步的.cpp文件编译成动态链接库文件;
  • 在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。

例1:以下声明合法的是:

A. default String s
B. public final static native int w()
C. abstract double d
D. abstract final double hyperbolicCosine()

答案:B
native可以和任何修饰符连用,abstract除外。因为native暗示这个方法时有实现体的,而abstract却显式指明了这个方法没有实现体。

例2:Which of the following can be applied to constructors?

A. final  B. static  C. synchronized  D. native  E. None of these.

答案:E

abstract方法和native方法都是方法的声明,一个把方法实现移交给子类,另一个移交给本地操作系统。
如果同时出现,就相当于既把实现移交给子类,又把实现移交给本地操作系统,那就会产生混乱。

Java构造方法可以有任何访问的修饰:public,protected,private或者没有修饰。
但是不能有以下非访问性质的修饰:abstract,final,native,static,或者synchronized。

例3:abstract方法是否可以同时是static的,是否可以同时是native方法,是否可以同时被synchronized修饰?

都不能。
抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
分享到 评论