JAVA学习笔记

摘要:

复习Java基础知识

Java程序的基本构成

  • 一个文件只能有一个public类,且这个类必须与文件名相同

数学函数、字符和字符串

  • Math是final类:在java.lang.Math中,所有数学函数都是静态方法。

    uMath类中定义了常用的数学常量,如

    PI : 3.14159265358979323846

    E : 2.7182818284590452354(自然对数的底)

    u方法:注意都是静态函数

    三角函数

    sin, cos, tan, asin, acos, atan,toRadians,toDigrees

    指数

    exp, log, log10,pow, sqrt

    取整

    ceil, floor, round

    其它

    min, max, abs, random([0.0,1.0))

    • 扩展:final关键字。

      • final修饰类时:当用final去修饰一个类的时候,表示这个类不能被继承。注意:a. 被final修饰的类,final类中的成员变量可以根据自己的实际需要设计为fianl。b. final类中的成员方法都会被隐式的指定为final方法。说明:在自己设计一个类的时候,要想好这个类将来是否会被继承,如果可以被继承,则该类不能使用fianl修饰,在这里呢,一般来说工具类我们往往都会设计成为一个fianl类。在JDK中,被设计为final类的有String、System等。

      • final修饰方法时:被final修饰的方法不能被重写,即不能被子类override。

        a. 一个类的private方法会隐式的被指定为final方法。

        b. 如果父类中有final修饰的方法,那么子类不能去重写。

      • final修饰成员变量时,必须初始化而且只能初始化一次。如果成员变量是引用变量,那么相当于和被引用的对象绑定,不能再引用其他对象,被引用对象的值可以改变。

      • final修饰形参时,表示方法内部不允许对这个参数进行修改,类似于C的const

      • 一个变量被final和static同时修饰时,可以用来表示常量,以此代替const关键字。并且此时如果不对变量进行赋值,自动按照默认值进行初始化

    • 扩展:静态函数与static关键字

      • static关键字;

        静态成员变量只会在数据共享区中维护一份,而非静态成员变量会在每个对象中维护一份

        作用

        static修饰成员变量:如果有数据需要共享所有数据使用时

        static修饰成员函数:如果一个函数没有直接访问非静态成员时,那么可以使用static修饰,一般用于工具类的方法(创建函数局部变量是ok的,不要和静态成员变量搞混了)

        访问方式

        1、 静态修饰成员变量与方法时,可以用类名或者对象进行访问

        2、非静态修饰成员变量与方法时,只能用对象进行访问

        静态函数的注意事项

        1、静态函数可以直接访问静态成员,但不能访问非静态成员。这是因为static方法是属于整个类的,所以他不能操纵和处理属于某个对象的成员变量

        2、非静态函数可以访问静态或者非静态成员

        3、静态函数不能出现this、super关键字。这是因为this,super关键字都是指向具体对象的,这与static是矛盾的

    • Java为每个基本类型实现了对应的包装类,char类型的包装类是Character类。注意包装类对象为引用类型,不是值类型

    • String类是一个final类,不能被继承。ujava.lang.String表示一个固定长度的字符序列,实例化后其内容不能改。

    • String m1 = “Welcome”; //字符串的内容都是不可修改的

      String m2 = “Welcome”; //m1和m2通过内存优化引用了同一常量对象:m1==m2

      String m3 = “Wel” +”come”; //m1==m2==m3

      String m4 = “Wel” +new String(“come”); //m1!=m4

      • 由于字符串是不可变的,为了提高效率和节省内存,Java中的字符串字面值维护在字符串常量池中)。这样的字符串称为规范字符串(canonical string)。

      • 可以使用字符串对象(假设内容为Welcome to Java)的intern方法返回规范化字符串。intern方法会在字符串常量池中找是否已存在”Welcome to Java”,如果有返回其地址。如果没有,在池中添加“Welcome to java”再返回地址。即intern方法一定返回一个指向常量池里的字符串对象引用。

      • 直接用字符串字面量构造的字符串在常量池里,如s。用new String方法构造的字符串在堆里,如s1。

        只有字面量在常量池里,例如:”Wel” + “come”,而”Wel”+new String(“come”)不在常量池里,在堆里。

      • 扩展:堆

        (1)Java的堆是一个运行时数据区,类的对象从堆中分配空间。这些对象通过new等指令建立,通过垃圾回收器来销毁。

        (2)堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。

        1)栈中主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和对象的引用。

        (2)栈的优势是,存取速度比堆快,栈数据可以共享。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。

  • 基本数据类型和字符串间的转换

    • valueOf方法将基本数据类型转换为字符串。例如

      String s1 = String.valueOf(1.0); //“1.0”

      String s2 = String.valueOf(true); //“true”

    • 字符串转换为基本类型:利用包装类

      Double.parseDouble(str)

      Integer.parseInt(str)

      Boolean.parseBoolean(str)

  • 字符串类型StringBuilder与StringBuffer

    • String类一旦初始化完成,字符串就是不可修改的。

    • StringBuilder与StringBuffer(final类)初始化后还可以修改字符串。

    • StringBuffer修改缓冲区的方法是同步(synchronized)的,更适合多线程环境。

    • StringBuilder线程不安全,与StringBuffer工作机制类似。

    • 由于可修改字符串, StringBuilder StringBuffer 增加了String类没有的一些函数,例如:append、insert、delete、replace、reverse、setCharAt等。

      仅以StringBuilder为例:

      StringBuilder stringMy=new StringBuilder( );

      StringMy.append(“Welcome to”);

      StringMy.append(“ Java”);

循环

  • JDK 1.5引入新的for循环,可以不用下标就可以依次访问数组元素。语法:

    for(elementType value : arrayRefVar) {

    }

    例如

    for(int i = 0; i < myList.length; i++) {

    sum += myList[i];

    }

for(double value : myList) {

sum += value;

}

方法

  • 方法中不能定义static局部变量
  • 每当调用一个方法时,系统将该方法参数、局部变量存储在一个内存区域中,这个内存区域称为调用堆栈(call stack)。当方法结束返回到调用者时,系统自动释放相应的调用栈
  • 方法重载(overloading)是指方法名称相同,但形参列表不同的方法。仅返回类型不同的方法不是合法的重载。一个类中可以包含多个重载的方法(同名的方法可以重载多个版本)。

数组

  • 数组用new创建后,内存单元都初始化为0(值)或null(引用)

  • 数组元素本身也可以是引用变量

    double[ ][ ] myList = new double[4][ ]; //创建一个二维数组

    myList[0]=new double[2]; // myList[0]是一个引用变量,指向一个一维数组(2个元素)

    myList[3]=new double[3]; // myList[3]是一个引用变量,指向一个一维数组(3个元素)

  • 数组变量是引用类型的变量,声明数组引用变量并不分配数组内存空间。必须通过new实例化数组来分配数组内存空间

  • String、Integer这样的对象作为参数传递要注意的问题:引用类型的实参传递给形参后,实参、形参指向同一个对象。但是,对于String类、基本数据类型的包装类型的实参传递给形参,形参变了不会导致实参变化。这是为什么?这是因为String、Integer的内容是不可更改的,在Integer内部,用private final int value来保存整数值,在String内部,用private final char value[] 来保存字符串内容。对于String、Integer这样内容不可改变的对象,当对其赋值时实际上创建了一个新的对象。

  • 可变长参数列表:可以把类型相同但个数可变的参数传递给方法。方法中的可变长参数声明如下

    typeName … parameterName

    在方法声明中,指定类型后面跟省略号

    只能给方法指定一个可变长参数,同时该参数必须是最后一个参数

    Java将可变长参数当数组看待,通过length属性得到可变参数的个数

    print(String… args){ //可看作String [ ]args

    for(String temp:args)

    System.out.println(temp);

    System.out.println(args.length);
    }

    调用该方法

    print(“hello”,”lisy”);

  • Arrays类

    ujava.util.Arrays类包括各种静态方法,其中实现了数组的排序和查找

    Ø排序

    double[ ] numbers={6.0, 4.4, 1.9, 2.9};

    java.util.Arrays.sort(numbers); //注意直接在原数组排序

    Ø二分查找

    ​ int[ ] list={2, 4, 7, 10, 11, 45, 50};

    int index = java.util.Arrays.binarySearch(list, 11);

对象和类

  • 当创建对象数组时,数组元素的缺省初值为null。

    Circle[] circleArray = new Circle[10]; //这时没有构造Circle对象,只是构造数组

    for(int i = 0; i < circleArray.length; i++) {

    circleArray[i] = new Circle( ); //这时才构造Circle对象,可使用有参构造函数

    }

  • 构造函数

    • 无返回类型,名字同类名,用于初始化对象。

    • 注意JAVA如果定义void className(…),被认为是普通方法

    只在new时被自动执行。

    • 必须是实例方法(无static),可为公有、保护、私有和包级权限。

    • 类的变量为引用(相当于C指针),指向实例化好的对象。

    Circle c2=new Circle(5.0);//调用时必须有括弧,可带参初始化

    • 缺省构造函数(同C++)

    如果类未定义任何构造函数,编译器会自动提供一个不带参数的默认构造函数。

    如果已自定义构造函数,则不会提供默认构造函数

  • Java无类似C++的&或C#的ref来修饰方法参数,只能靠形参的声明类型来区分是传值还是传引用,因此一定要注意区分

    • 包是一组相关的类和接口的集合。将类和接口分装在不同的包中,可以避免重名类的冲突,更有效地管理众多的类和接口。因此package就是C++里的namespace

    • 包的定义通过关键字package来实现的 ,package语句的一般形式:

      package 包名;

      package语句必须出现在.java文件第一行,前面不能有注释行也不能有空白行,该.java文件里定义的所有内容(类、接口、枚举)都属于package所定义的包里。如果.java文件第一行没有package语句,则该文件定义的所有内容位于default包(缺省名字空间),但不推荐。

      不同.java文件里的内容都可以属于同一个包,只要它们第一条package语句的包名相同

    • package本质上就是C++里的namespace,因此

      在同一个package里不能定义同名的标识符(类名,接口名,枚举名)。例如一个类名和一个接口名不能相同

      如果要使用其它包里标识符,有二个办法:

      用完全限定名,例如要调用java.util包里的Arrays类的sort方法: java.util.Arrays.sort(list);

      在package语句后面,先引入要使用其它包里的标识符,再使用:

      import java.util.Arrays; //或者: import java.util.*;

      Arrays.sort(list);

      import语句可以有多条,分别引入多个包里的名字

  • 包的命名习惯: 将Internet域名作为包名 (但级别顺序相反),这样的好处是避免包名的重复

    org.apache.tools.zip

    cn.edu.hust.cs.javacourse.ch1

    如果所有程序员都遵循这种包命名的约定,包名重复的可能性就非常小

  • 数据成员的封装

    • 面向对象的封装性要求最好把实例成员变量设为私有的或保护的

    • 同时为私有、保护的实例成员变量提供公有的get和set方法。get和set方法遵循JavaBean的命名规范

    • 设成员为DateType propertyName。

    • get用于获取成员值:public DateType getPropertyName( );

    • set用于设置成员值:public void setPropertyName(DateType value)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Circle{

    private double radius=1.0; //数据成员设为私有

    public Circle( ){ radius=1.0; }

    public double getRadius( ){ return radius; }

    public void setRadius(double r){ radius=r; }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Circle {
    private double radius;
    /** 私有静态变量,记录当前内存里被实例化的Circle对象个数*/
    private static int numberOfObjects = 0;

    public Circle() { radius = 1.0; numberOfObjects++; }
    public Circle(double newRadius) { radius = newRadius; numberOfObjects++; }

    public double getRadius() {return radius;}
    public void setRadius(double newRadius) { radius = newRadius;}
    /** 公有静态方法,获取私有静态变量内容*/
    public static int getNumberOfObjects() {return numberOfObjects;}

    /** Return the area of this circle */
    public double findArea() { return radius * radius * Math.PI; }
    /*覆盖从Object继承的finalize方法,该方法在对象被回收时调用,方法里对象计数器-1。注意该方法调用时机不可控制。 @Override是注解(annotation)
    告诉编译器这里是覆盖父类的方法。
    */
    @Override
    public void finalize() throws Throwable {
    numberOfObjects--; //对象被析构时,计数器减1
    super.finalize();
    }
    }
  • 方法重载(overload)、方法重写(override)、方法隐藏

    • 方法重载:同一个类中、或者父类子类中的多个方法具有相同的名字,但这些方法具有不同的参数列表(不含返回类型,即无法以返回类型作为方法重载的区分标准)

    • 方法重写和方法隐藏:发生在父类和子类之间,前提是继承。子类中定义的方法与父类中的方法具有相同的方法名字、相同的参数列表、相同的返回类型(也允许子类中方法的返回类型是父类中方法返回类型的子类)

    • 方法重写:实例方法

    • 方法隐藏:静态方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public class A {
      public void m(int x, int y) {}
      public void m(double x, double y) {}

      //下面语句报错m(int,int)已经定义, 重载函数不能通过返回类型区分
      // public int m(int x, int y) { return 0;};
      }

      class B extends A{ //B继承了A
      public void m(float x, float y) { } //重载了父类的m(int,int)和m(double,double)
      public void m(int x, int y) {} //覆盖了父类的void m(int,int),注意连返回类型都必须一致

      //注意下面这个语句报错,既不是覆盖(与父类的void m(int,int)返回类型不一样)
      // 也不是合法的重载(和父类的m(int,int)参数完全一样,只是返回类型不一致
      // public int m(int x, int y) {} //错误

      //子类定义了新的重载函数int m()
      public int m(){return 0;};
      }
  • 可见性修饰符

    • Java继承时无继承控制(见继承,即都是公有继承,和C++不同),故父类成员继承到派生类时访问权限保持不变。

    • 成员访问控制符的作用:

      • private: 只能被当前类定义的函数访问。

      • 包级:只能被同一包中的类访问(成员没有显示表明访问修饰符时,默认访问控制符为包级packdge)

      • protected:直接子类(是否同一包中均可)、同一包中的类的函数可以访问。

      • public: 所有类的函数都可以访问。

      • 注意:若不加访问控制符,则默认为包级,而包级只能被同一包中的类访问,不能被子类访问

继承和多态

  • 如果class C1 extends C2,则称C1为子类(subclass),C2为父类(superclass)。

  • 子类继承了父类中可访问的数据和方法,子类也可添加新的数据和方法

  • 子类不继承父类的构造函数。

  • 一个类只能有一个直接父类(Java不支持多重继承,因为Java的设计者认为没有必要)

  • Java的继承都是公有继承,因此被继承的就是父类,继承的类就是子类。因此父类的成员如果被继承到子类,访问权限不变

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    public class GeometricObject { //等价于public class GeometricObject extends Object
    private String color = "white";
    private boolean filled;
    private Date dateCreated; //java.util.Date是JDK定义的类,表示日期和时间
    public GeometricObject() { dateCreated = new Date();}

    public String getColor() { return color; }
    public void setColor(String color) { this.color = color;}
    public boolean isFilled() { return filled; }
    public void setFilled(boolean filled) { this.filled = filled;}
    public Date getDateCreated() {return dateCreated;}

    @Override //覆盖Object类的toString()方法
    public String toString( ) { //还应考虑equals,clone
    return "created on " + dateCreated + "\n\tcolor: " + color
    + " and filled: " + filled;
    }//toString方法应该返回一个描述当前对象的有意义的字符串
    }

    public class Circle extends GeometricObject {
    private double radius; //新增属性
    public Circle() { }
    public Circle(double radius) { this.radius = radius; }
    public double getRadius() { return radius; }
    public void setRadius (double radius) { this.radius = radius; }

    public double getArea() {
    return radius * radius * Math.PI;
    }
    public double getDiameter() {
    return 2 * radius;
    }
    public double getPerimeter() {
    return 2 * radius * Math.PI;
    }
    //还应考虑equals,clone,toString等函数
    }

    public class Rectangle extends GeometricObject {
    private double width;
    private double height;

    public Rectangle() { }
    public Rectangle(double width, double height) {
    this.width = width;
    this.height = height;
    }

    public double getWidth() { return width; }
    public void setWidth (double width) { this.width = width;}
    public double getHeight() { return height;}
    public void setHeight (double height) { this.height = height;}
    public double getArea() { return width * height;}
    public double getPerimeter() {
    return 2 * (width + height);
    }
    //还应考虑equals,clone,toString等函数
    }
  • 实例初始化模块

    • 初始化块是Java类中可以出现的第四种成员(前三种包括属性、方法、构造函数),分为实例初始化块和静态初始化块。

    • 实例初始化模块(instance initialization block,IIB)是一个用大括号括住的语句块,直接嵌套于类体中,不在方法内。

    • 它的作用就像把它放在了类中每个构造方法的最开始位置。用于初始化对象。实例初始化块先于构造函数执行

    • 作用:如果多个构造方法共享一段代码,并且每个构造方法不会调用其他构造方法,那么可以把这段公共代码放在初始化模块中。

    • 一个类可以有多个初始化模块,模块按照在类中出现的顺序执行

    • 下面的两段代码等价

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class Book{
      private static int numOfObjects;
      private String title
      private int id;
      public Book(String title){
      this.title = title;
      }
      public Book(int id){
      this.id = id
      }

      {
      numOfObjects++;
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class Book{
      private static int numOfObjects;
      private String title
      private int id;
      public Book(String title){
      numOfObjects++;
      this.title = title;
      }
      public Book(int id){
      numOfObjects++;
      this.id = id
      }
      }
      • 实例初始化模块最重要的作用是当我们需要写一个内部匿名类时:匿名类不可能有构造函数,这时可以用实例初始化块来初始化数据成员.•实例初始化模块还有个作用是可以截获异常

      • 实例初始化模块只有在创建类的实例时才会被调用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        public class Book{
        private int id = 0; //执行次序:1
        public Book(int id){ //执行次序:4
        this.id = id
        }
        {
        //实例初始化块 //执行次序:2
        }
        {
        //实例初始化块 //执行次序:3
        }
        }
  • 静态初始化模块

    • •静态初始化模块是由static修饰的初始化模块{},只能访问类的静态成员,并且在JVM的Class Loader将类装入内存时调用。(类的装入和类的实例化是两个不同步骤,首先是将类装入内存,然后再实例化类的对象)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public class Book{
      private static int id = 0; //执行次序:1
      public Book(int id){
      this.id = id
      }
      static {
      //静态初始化块 //执行次序:2
      }
      static {
      //静态初始化块 //执行次序:3
      }
      }
  • 初始化模块执行顺序

    • 第一次使用类时装入类

      • 如果父类没装入则首先装入父类,这是个递归的过程,直到继承链上所有祖先类全部装入

      • 装入一个类时,类的静态数据成员和静态初始化模块按它们在类中出现的顺序执行

    • 实例化类的对象

      • 首先构造父类对象,这是个递归过程,直到继承链上所有祖先类的对象构造好
      • 构造一个类的对象时,按在类中出现的顺序执行实例数据成员的初始化及实例初始化模块
      • 执行构造函数函数体
    • 例子(输出为012345678)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      public class InitDemo{
      InitDemo(){
      new M();
      }
      public static void main(String[] args){
      System.out.println("(1) ");
      new InitDemo();
      }
      {
      System.out.println("(2) ");
      }
      static{
      System.out.println("(0) ");
      }
      }

      class N{
      N(){ System.out.println("(6) "); }
      {
      System.out.println("(5) ");
      }
      static {
      System.out.println("(3) ");
      }
      }
      class M extends N{
      M(){ System.out.println("(8) "); }
      {
      System.out.println("(7) ");
      }
      static {
      System.out.println("(4) ");
      }
      }

    匿名内部类

  • 匿名内部类也就是没有名字的内部类

    正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写

    但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口

  • 一次性使用的类

  • 匿名内部类不能定义构造方法,因为它没有名字

  • 在构造对象时使用父类的构造方法

实例1:不使用匿名内部类来实现抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Person {
public abstract void eat();
}

class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}

public class Demo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}

运行结果:eat something

可以看到,我们用Child继承了Person类,然后实现了Child的一个实例,将其向上转型为Person类的引用

但是,如果此处的Child类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?

这个时候就引入了匿名内部类

实例2:匿名内部类的基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Person {
public abstract void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}

运行结果:eat something

可以看到,我们直接将抽象类Person中的方法在大括号中实现了

这样便可以省略一个类的书写

并且,匿名内部类还能用于接口上

实例3:在接口上使用匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
public void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}

运行结果:eat something

由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现

最常用的情况就是在多线程的实现上,因为要实现多线程必须继承Thread类或是继承Runnable接口

实例4:Thread类的匿名内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
t.start();
}
}

运行结果:1 2 3 4 5

实例5:Runnable接口的匿名内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
Thread t = new Thread(r);
t.start();
}
}

运行结果:1 2 3 4 5

  • super关键字

    • 利用super可以显式调用父类的构造函数

      • super(parametersopt)调用父类的的构造函数。

      • 必须是子类构造函数的第1条且仅1条语句(先构造父类)。

      • 如果子类构造函数中没有显式地调用父类的构造函数,那么将自动调用父类不带参数的构造函数。

      • 父类的构造函数在子类构造函数之前执行。

    • 调用父类的成员(这时super类似于this引用,只不过是指向父类对象)

      • super.data(如果父类属性在子类可访问)

      • super.method(parameters)(如果父类方法在子类可访问)

      • Circle代码里可以写getDataCreated()或super.getDataCreated()

      • 不能使用super.super.p()这样的super链

    • 编译器在为子类添加无参构造函数时,函数体里会用super( )默认调用父类的无参构造函数,如果找不到父类无参构造函数,则编译器为子类添加无参构造函数失败,编译报错。如果一个类定义了带参数的构造函数,一定别忘了定义一个无参的构造函数,原因是:由于系统不会再自动加上无参构造函数,就造成该类没有无参构造函数,那么在被继承时如果子类构造函数没有显式调用父类的带参构造函数,就会自动执行父类的无参构造函数,但是父类没有无参构造函数,这时就会出错

  • 多态

    • 多态:通过引用变量调用实例函数时,根据所引用的实际对象的类型,执行该类型的相应实例方法,从而表现出不同的行为称为多态。通过继承时覆盖父类的实例方法实现多态。多态实现的原理:在运行时根据引用变量指向对象的实际类型,重新计算调用方法的入口地址(晚期绑定)。

    • 例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class Person{  void Greeting( ){ System.out.println(“Best wish from a person!"); } }
      class Employee extends Person
      { void Greeting( ){ System.out.println(“Best wish from a employee!");} }
      class Manager extends Employee{
      void Greeting( ){ System.out.println(“Best wish from a manager!");} }

      public class GreetingTest1{
      public static void main(String[] args){
      //父类引用变量可以引用本类和子类对象,p1,p2,p3的声明类型都是Person(父类型),p2,p3执行子类对象
      Person p1= new Person( ),p2= new Employee( ),p3= new Manager( );
      p1.Greeting( ); //调用Person的Greeting() ,由于实际指向对象类型是Person
      p2.Greeting( ); //调用Employee的Greeting() ,由于实际指向对象类型是Employee
      p3.Greeting( ); //调用Manager的Greeting() ,由于实际指向对象类型是Manager

      }
      }
    • 另一个例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class GreetingSender{
      public void newYearGreeting (Person p){ p.Greeting(); //编译时应该是Person的Greeing}
      }
      public class GreetingTest1{
      public static void main(String[] args){
      GreetingSender g = new GreetingSender();
      g.newYearGreeting(new Person()); //调用Person的Greeting()
      g.newYearGreeting(new Employee()); //调用Employee的Greeting()
      g.newYearGreeting(new Manager()); //调用Manager的Greeting()
      }
      }
      /*以最后一条语句为例来解释多态特性:
      当实参new Manager()传给形参Person p时,等价于Person p = new Manager(), 因此执行p.Greeting()语句时根据形参p指向的对象的实际类型动态计算Greeting方法的入口地址,调用了Manager的Greeting()*/
      • 这段程序的微妙之处在于:

        GreetingSender类的newYearGreeting方法的参数是Person类型,那么

        newYearGreeting的行为应该是Person对象的行为。

        但是在实际运行时我们看到随着实参对象类型的变化, newYearGreeting

        方法却表现出了多种不同的行为,这种机制称为多态

      • 仔细观察程序,可以发现产生多态的三个重要因素:

        1:不同类之间有继承链

        2:newYearGreeting方法的参数类型用的父类类型

        3:newYearGreeting调用的Greeting方法都被子类用自己的行为覆盖

        满足了这三个条件,用继承链中不同子类的对象做为方法的实参去调用方法会使该方法表现出不同的行为。由于子类的实例也是父类的实例,所以用子类对象作为实参传给方法中的父类型的形参是没有问题的。

    • 当调用实例方法时,由Java虚拟机动态地决定所调用的方法,称为动态绑定(dynamic binding)或者晚期绑定或者延迟绑定(lazy binding)或者多态。

    • 通用编程:方法参数和变量的声明类型越抽象越好,越抽象越通用

    • 当编译器检查到 Manager m = p;编译器认为Person类型引用p要赋值给类型为Manager类型引用,扩展内存可能引起麻烦且不安全,因此,编译器认为类型不匹配,会报错。

      加上强制转换 Manager m = (Manager)p;意思是强烈要求编译器,把p解释成Manager类型,风险我来承担。这个时候编译器就按Manager类型来解释p

      因此,强制类型转换意味着你自己承担风险,编译器不会再做类型检查。

      强制类型转换的风险是:运行时如果p指向的对象不是Manager的实例时程序会出错。

      为了避免风险,最好用instanceof来做实例类型检查。

      1
      2
      if(p instanceof Manager)
      Manager m = (Manager)p;
    • 重载发生在编译时(Compile time),编译时编译器根据实参比对重载方法的形参找到最合适的方法。

      多态发生在运行(Run time)时,运行时JVM根据变量所引用的对象的真正类型来找到最合适的实例方法。

      有的书上把重载叫做“编译时多态”,或者叫“早期绑定”(早期指编译时)。

      多态是晚期绑定(晚期指运行时)

      绑定是指找到函数的入口地址的过程。

文本IO

  • 文本:非二进制文件(参见FileInputStream、FileOutputStream)。

    类库:java.io.File、java.util.Scanner、java.io.PrinterWriter。

    类File: 对文件和目录的抽象,包括:路径管理,文件读写状态、修改日期获取等。

    类Scanner:从File或InputStream的读入。可按串、字节、整数、双精度、或整行等不同要求读入。

    类PrinterWriter : 输出到File或OutputStream: 可按串、字节、整数、双精度、或整行等不同要求输出。

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package  filecopy;
    import java.lang.System;
    import java.io.File;
    import java.io.PrintWriter;
    import java.io.IOException;
    import java.util.Scanner;
    public class Copy {
    public static void main(String[ ] args) { //参数不含程序名
    if(args.length!=2){
    System.out.println("Usage: Java Copy <sourceFile> <tagetFile>");
    System.exit(1);
    };
    File sF=new File(args[0]); //args[0]:源文件路径
    if(!sF.exists( )){
    System.out.println("Source Fiel "+args[0]+ "does not exist!");
    System.exit(2);
    };
    File tF=new File(args[1]); //args[1]:目标文件
    if(tF.exists( )){
    System.out.println("Target File "+args[0]+ "already exist");
    System.exit(3);
    };
    try{
    Scanner input=new Scanner(sF);
    PrintWriter output=new PrintWriter(tF);
    while(input.hasNext( )){
    String s=input.nextLine(); //读取下一行
    output.println(s); //打印这一行
    }
    input.close( );
    output.close( );
    }
    catch(IOException ioe){
    System.out.println(ioe.toString( ));
    }
    }
    }

异常处理

  • Exception分为两种,一种是RuntimeException及其子类,可以不明确处理。其余的称为受检的异常。受检的异常要求进行明确的语法处理。处理方式有两种,要么catch,要么throws。如果是throws的话,要在方法的签名后面用throws xxxx来声明。
    • 在子类中,如果要覆盖父类的一个方法,若父类中的方法声明了throws异常,则子类方法也可以throws异常。
    • 可以抛出子类异常(更为具体的异常),但是不能抛出更一般的异常

抽象类和接口

  • Java可定义不含方法体的方法,其方法体由子类根据具体情况实现,这样的方法称为抽象方法(abstract method),包含抽象方法的类必须是抽象类(abstract class)。

  • 抽象类和抽象方法的声明必须加上abstract关键字。

  • 抽象方法的意义:加给子类的一个约束。例如Circle类和Rectangle类计算面积必须使用父类规定的函数签名。这样可以充分利用多态特性使得代码变得更通用

  • 抽象方法:使用abstract定义的方法或者接口中定义的方法(接口中定义的方法自动是抽象的,可以省略abstract)

  • 一个类C如果满足下面的任一条件,则该类包含抽象方法且是抽象类:

    • 类C显式地包含一个抽象方法的声明;

    • 类C的父类中声明的抽象方法未在类C中实现;

    • 类C所实现的接口中有的方法在类C里没有实现

    • 只要类C有一个未实现的方法(自己定义的或继承的),就是抽象类

    • 但是,一个不包含任何抽象方法的类,也可以定义成抽象类

  • 抽象类不能被实例化,即不能用new关键字创建对象(即new 右边的类型不能是抽象类)。

    但是抽象类可以作为变量声明类型、方法参数类型、方法返回类型

    为什么?因为一个抽象类型引用变量可以指向具体子类的对象

    抽象类可以定义构造函数,并可以被子类调用。

    抽象类可以定义变量、非抽象方法并被子类使用

    抽象类的父类可以是具体类:自己引入了抽象方法。例如,具体类Object是所有类的祖先父类。

  • 接口是公共静态常量和公共抽象实例方法的集合。接口是能力、规范、协议的反映。

    接口不是类:(1)不能定义构造函数;(2)接口之间可以多继承,类可implements多个接口。(3)和抽象类一样,不能new一个接口

  • 接口中的所有数据字段隐含为public static final

    接口体中的所有方法隐含为public abstract

  • 接口不是类(Java支持单继承类),一个接口可以继承多个接口。

    语法

    [modifier*] interface *interfaceName [extends interfaceName*List*] {

    declaration*

    }

    如果接口声明中提供了extends子句,那么该接口就继承了父接口的方法和常量。被继承的接口称为声明接口的直接父接口。

    任何实现该接口的类,必须实现该接口继承的其他接口

  • 当使用public修饰接口时,表示任何一个类都可以使用这个接口。缺省情况下,只有与该接口定义在同一个包中的类才可以访问这个接口

  • 一个接口可以继承多个接口,使用extends,多个接口之间使用逗号隔开。

内部类

  • 定义:将类的定义class xxx{..}置入一个类的内部即可
  • 内部类不能与外部类同名
  • 在封装内部类的类内部使用内部类,与普通类的使用方式相同。在其他地方使用内部类时,类名前要加上外部类的名字。在用new创建内部类时,也要在new前面加上对象变量。即外部对象名.new 内部类名(参数)
  • 内部类中可以直接访问外部类的字段和方法,即使是private也可以。如果内部类有和外部类同名的字段或者方法,可以使用外部类名.this.字段或方法来访问外部类。

局部类

  • 在一个方法中定义的类,叫做局部类。
  • 同局部变量一样,局部类不能使用public private protected static修饰,但是可以被final abstract修饰。可以访问其外部类的成员,但是不能访问局部类所在的方法的局部变量,因为方法的局部变量是可变的,除非是final修饰的局部变量。

匿名类

  • 与匿名内部类相同。参见前面的匿名内部类

Java语言基础类

  • “==”是引用相等,equals是内容相等

泛型

  • 泛型类型必须是引用类型,不能使用int double char这样的基本类型来替换泛型类型。例如下面的语句是错误的。ArrayList intList = new ArrayList<>()。为了给int值创建一个ArrayList对象,必须使用ArrayList intList = new ArrayList<>()

  • 泛型类的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import java.util.ArrayList;
    public class GenericStack<E> {
    private ArrayList<E> list = new ArrayList<E>();
    public boolean isEmpty() {
    return list.isEmpty();
    }
    public int getSize() {
    return list.size();
    }
    public E peek() {
    return list.get(getSize() - 1);//取值不出栈
    }
    public E pop() {
    E o = list.get(getSize() - 1) ;
    list.remove(getSize() - 1);
    return o;
    }
    public void push(E o) {
    list.add(o);
    }
    public String toString() {
    return "stack: " + list.toString();
    }
    }
  • 如果不使用泛型,而将上面的元素类型都设置为Object,也可以容纳任何类型的对象。但是,使用泛型能够提高软件的可靠性和可读性,因为某些错误能够在编译时而不是运行时被检测到。这也是使用泛型的主要原因。

  • 泛型方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class GenericMethodDemo {
    public static void main(String[] args) {
    Integer[] integers = {1,2,3,4,5};
    String[] strings = {"Londen","Paris","New York","Austin"};
    GenericMethodDemo.<Integer>print(integers);
    GenericMethodDemo.<String>print(strings);
    }
    public static <E> void print(E[] list){
    for(int i = 0 ; i <list.length; i++){
    System.out.print(list[i]+" ");
    System.out.println();
    }
    }
    }

    声明泛型方法,将类型参数置于返回类型之前

    调用泛型方法,将实际类型放于<>之中方法名之前;也可以不显式指定实际类型,而直接给实参调用,如print(integers); print(strings);由编译器自动发现实际类型