Head First Java笔记


文章目录

  • Head First Java笔记
  • Java API(使用函数库)
  • 继承与多态
  • 接口与抽象类
  • 构造器与垃圾收集器
  • 数字与静态
  • 异常处理
  • 图形用户接口
  • swing
  • 序列化和文件的输入/输出
  • 网络与线程
  • 集合与泛型
  • 包、jar存档文件和部署
  • 远程部署的RMI


Java API(使用函数库)

  1. Java的API中,类被包装在包中。
  2. 除Java.lang这个包,使用到其他包的类必须指定全名。

import:import述句在程序源文件最前端。
import java.util.ArrayList type:程序代码中使用全名。
java.util.ArrayList<Dog> List=new java.util.ArrayList<Dog>();

继承与多态

  1. “子类继承父类”,继承即子类继承了父类的方法。(子类是extend 父类出来的)
  2. “类的成员”,成员指实例变量和方法。当调用对象引用的方法时,会调用到与该对象类型最接近的方法,即Java虚拟机会从层次树的最下方开始找起。
  3. IS-A测验:“是一个”VS“有一个”。测验是否是继承关系,A是B,则A继承B。
    eg:“三角形是多边形”,即三角形可继承多边形。
    “浴室有一个澡盆”,即浴室带有澡盆的实例变量。
  4. “super”关键字——可用于取用父类。(子类不用完全覆盖掉父类功能,只加上额外行为)
public void roam(){
           super.roam();    //调用父类的roam方法
           //my own roam stuff
 }
  1. 继承下来的方法可被覆盖掉,但实例变量不能被覆盖掉。
  2. 多态:同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作
  3. Dog myDog = new Dog();,声明一个引用变量并创建对象,引用类型与对象类型必须相符。
    Animal myDog = new Dog();,但在多态下,引用与多态可以是不同类。
    编译器会根据引用的类型而不是实际对象来判定你能够调用哪些方法
  4. 运用多态时,引用类型可以是实际对象类型的父类。即任何extend过声明引用变量类型的对象都可被赋值给这个引用对象。
Animal [] animals=new Animal[2];
animals [0]=new Dog();
animals [1]=new Cat();    //可放任何Animal子类对象进去
for (int i=0;i<animals.length;i++){    //多态,可将数组的元素逐个调出,当做Animal来操作
     animal[i].eat();
     animal[i].roam();
};

参数和返回类型也可以多态,如将参数声明为父类类型,则即可在运行时传入任何的子类对象。

  1. 覆盖父类的方法,需保证:
  • 参数必须要一样,且返回类型必须要兼容。
  • 不能降低方法的存取权限
  1. 方法的重载:两个方法的名称相同,但参数不同。(发生在同一个类中)
  • 返回类型可以不同(只要所有覆盖使用不同参数即可)
  • 不能只改变返回类型(可以只改变参数项,或同时改变参数值和返回值项)
  • 可以更改存取权限
public class Overloads{
   String uniqueID;
   public int addNums(int a,int b){
     return a+b; }
   public double addNums(double a,double b){
     return a+b; }
   public void setUniqueID(String theID){
     uniqueID=theID; }
   public void setUniqueID(int ssNumber){
     String numString=""+ssNumber;
     setUniqueID(numString); }
}

接口与抽象类

  1. 关键字: abstract
  2. 具体类可以被初始化为对象。
  • 抽象类不能创造出该类的实例,可使用抽象类来声明为引用类型给多态使用。
  • 抽象类除了被继承过之外,是没有用途、值和目的。
  1. 抽象方法也可以标记为abstract。
    public abstract void eat(); //抽象的方法没有实体,不含方法体 抽象类代表此类必须要被extend过,抽象的方法代表此方法一定要被覆盖过。
    若声明一个抽象方法,就必须要将类标记为抽象类,即不能在非抽象类中拥有抽象方法

抽象方法意义:就算无法实现方法内容,但可定义出一组子型共同的协议,有利于多态的使用,所有子型都会拥有抽象的方法。

  1. 继承树结构下的第一个具体类必须要实现出所有抽象方法
    必须以相同的方法鉴名(名称与参数)和相同的返回类型创建出非抽象的方法。
  2. Java中所有类都是从Object这个类继承出来的,是所有类的父类。
  3. 不管实际上所引用的对象是什么类型,只有在引用变量的类型就是带有某方法的类型时才能调用该方法。

任何从ArrayList取出的Object都会被当做Object这个类的实例,而不管它原来是什么。

  1. 对象类型转换

    若确定为Dog对象,则从Object中拷贝出一个Dog引用,并赋值给Dog引用变量。
    若不确定为Dog对象,则使用instanceof运算符检查。若类型转换错误,在执行时会遇到ClassCastException异常并终止。
object o = al.get(index);
    if (o instanceof Dog){
        Dog d = (Dog) o;    //将类型转换成Dog
    }
    d.roam();
  1. Java不支持多重继承。
  2. 接口是抽象方法的集合,所有接口的方法都是抽象的,通常以interface声明。
    一个类通过继承接口的方式,从而来继承接口的抽象方法。
    public interface 接口名称 {...} //接口的定义
  3. 接口的实现
    当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。
    类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。
    public class Dog extends Canine implements Pet {...} //接口的实现 实现某接口的类必须实现它所有的方法,因为这些方法都是public和abstract的。
    类可以实现多个接口。
  • 当把一个类作为多态类型时,相同的类型必定来自同一个继承树,而且必须是该多态类型的子类。
  • 当把接口作为多态类型时,对象可来自任何地方。唯一条件是该对象必须是来自有实现该接口的类,允许不同继承树的类实现共同的接口。
  1. 从子类调用父类的方法(即不完全覆盖父类方法,只加入额外动作)可以用super关键字进行引用。
abstract class Report{
    void runReport(){    //父类方法,带有子类可运用的部分
      //设置报告
    }
}
class BuzzwordsReport extends Report{
    void runReport(){
        super.runReport();   //调用父类的方法
        buzzwordCompliance();
        printReport();
    }
}

构造器与垃圾收集器

  1. 中存放所有的对象,又称为可垃圾回收的堆。
    中存放方法调用和局部变量。
  2. 变量分为实例变量和局部变量。
  • 实例变量被声明在类而不是方法中,代表每个独立对象的字段(每个实例都能有不同的值),实例变量存在于所属对象的堆空间中。
public class Duck{
   int size;   //每个Duck对象都会有独立的size
}
  • 局部变量又称为栈变量,局部变量和方法的参数都是被声明在方法中,它们是暂时的,生命周期只限于方法被放在栈上期间(即方法调用至执行完毕为止)。
public void foo (int x)
    int i=x+3;    
    boolean b=ture;   //参数x和变量i,b均为局部变量
  1. 非primitive的变量只是保存对象的引用而已,而不是对象本身。
    对象引用变量与primitive主数据类型变量都放在栈上。
  2. 无论是实例变量或局部变量,对象本身只会存在于堆上。
  3. 若Cellphone对象带有一个Antenna对象(即Cellphone带有Antenna类型的引用变量),Java只会留下Antenna引用量而不是对象本身所用到的空间。
  • Antenna对象若有声明变量但没有给它赋值,则只会留下变量的空间。
    pritive Antenna ant;
  • 直到引用变量被赋值一个新的Antenna对象才会在堆上占有空间。
    pritive Antenna ant = new Antenna();
  1. 构造函数
    Duck myDuck = new Duck(); //在调用Duck的构造函数。 构造函数带有在初始化对象时会执行的程序代码,即新建一个对象时就会被执行。
    public Duck {...} //构造函数代码 构造函数无返回类型,且一定与类名相同。
  2. 构造函数会在对象能够被赋值给引用之前就执行。常被使用来初始化对象的状态,即设置和给对象的实例变量赋值。
    最好的方法是将初始化的程序代码放在构造函数中,把构造函数设置为需要参数的。
public class Duck{
    int size;
    public Duck(int ducksize){     //给构造函数加参数
        size = ducksize;      //使用参数值来设定size这个实例变量
        System.out.println("size is"+size);
    }
}
public class useADuck{
    public static void main(String[] args){
        Duck d = new Duck(42);     //传值给构造函数
    }   
}
  1. 若你没有自己写构造函数,编译器会帮你写一个,默认的构造函数没有参数。
    若你已经手动写了一个带参数的构造函数,则编译器不会调用,若你需要一个没有参数的构造函数,则必须自己手动写。
  2. 若类有一个以上的构造函数,即构造函数重载,则参数必须不同,包括参数的顺序与类型。
    public Duck () { }public Duck (int size) { }public Duck (String name) { }public Duck (String name,int size) { }public Duck (int size,String name) { } //类型相同但顺序不同
  3. 在创建新对象时,所有继承下来的构造函数都会执行。
    构造函数在执行时,首先是去执行它父类的构造函数,连锁反应到Object这个类为止。
  4. 抽象类也有构造函数。虽然对抽象类不能执行new操作,但抽象类还是父类,它的构造函数会在具体子类创建出实例时执行。
  5. 调用父类构造函数的唯一方法是调用super()。对super()的调用必须是构造函数的第一个语句。
public class Duck extends Animal{
    int size;
    
    public Duck(int newSize){
        super();     //调用父类构造函数
        size=newSize;
    }
}

若我们没有调用super(),编译器会帮加上super()的调用。编译器帮加的一定是没有参数的版本,若父类有多个重载版本,只有无参数版本会被调用。

  1. 使用this()来从某个构造函数调用同一个类的另外一个构造函数。
    this()只能用在构造函数中,且必须是第一行语句。super()和this()不能同时调用。
cless Mini extends Car{
    Color color;
    public Mini(){
        this(Color.Red);  //无参构造函数以默认的颜色调用真正的构造函数
    }
    public Mini(Color c){
        super("Mini");   //真正的构造函数
        color=c;
        //初始化动作
    }
}
  1. 引用变量的生命周期:
  • 局部变量只会存活在声明该变量的方法中,局部变量只能在声明它的方法中执行才能被使用。
public class TestLifeOne{

    public void read(){
        int s=42;  //s的范围只限于read()里,read()方法结束时s完全消失
        sleep();  
    }
    public void sleep(){
        s=7;    //非法使用!
    }
}
  • 实例变量的寿命与对象相同
public class Life{
    int size;
    
    public void setSize(int s){
        size=s;  //s在方法结束时消失,但size在类中,到处都可用
    }
}
  1. 对象的生命周期由引用变量的生命周期而定。当最后一个引用消失时,对象就会变成可回收的。有三种方式可以释放对象的引用:
  • 引用永久性的离开它的范围
    void go() { Life z=new Life(); } //z会在方法结束时消失
  • 引用被赋值到其他的对象上
    Life z=new Life();z=new Life(); //第一个对象会在z被赋值到别处时挂掉
  • 直接将引用设定为null
    Life z=new Life();z=null; //第一个对象会在z被赋值为null时击毙

数字与静态

  1. Math类中所有方法都不需要实例变量值。这些方法都是静态的,所以无需Math的实例,用到的只有它的类本身。
    int x=Math.round(42.2);int y=Math.min(56,12); //这些方法无需实例变量,因此无需特定对象来判别行为
  2. 非静态方法VS静态方法
    static关键词可以标记出不需类实例的方法,即静态方法。
  • 以类的名称调用静态方法
    Math.min(88,86); 以引用变量的名称调用非静态方法
    Song t2=new Song();t2.play();
  • 静态方法不能调用非静态的变量和方法。
  • 静态变量的值对所有的实例来说都相同。即,静态变量是共享的,同一类所有的实例共享一份静态变量。
    实例变量:每个实例一个
    静态变量:每个类一个
public class Duck{  //每个Duck对象都有自己的size变量,但所有Duck实例只有一份duckCount变量
    private int size;
    private static int duckCount=0;  //此静态duckCount变量只会在类第一次载入时被初始化
    public Duck(){
        duckCount++;  //每当构造函数执行时,此变量的值就会递增
    }
    public void setSize(int s){
        size=s;
    }
    public int getSize(){
        return size;
    }
}
  1. 如果类只有静态的方法,你可以将构造函数标记为private以避免被初始化。
    构造函数不可以标记为静态。
  2. 静态变量是在类被加载时,在该类的任何静态方法执行之前就初始化的。
  3. 静态final变量是常数,加载后一直维持原值。常数变量的名称应该都是大写字母。
    public static final double PI=3.1415926 public:可供各方读取
    static:因此不需要Math的实例
    final:圆周率是不变的
  4. 静态final变量的初始化:
  • 声明的时候
public class Foo{
    public static final int FOO_X=25;   //命名惯例:都是大写,以下划线字符分隔
}
  • 静态初始化程序中 (静态初始化程序是一段在加载类时会执行的程序代码,会在其他程序可以使用该类之前就执行,很适合放静态final变量的起始程序)
public class Bar{
    public static final double BAR_SIGN;
    
    static{    //这段程序会在类被加载时执行
        BAR_SIGN=(double)Math.random();
    }
}
  1. final也可用来修饰非静态的变量(包括实例变量、局部变量或方法的参数),方法及类。
    final的变量代表你不能改变它的值。
    final的方法表示你不能覆盖掉该方法。
    final的类代表你不能继承该类(即创建它的子类)。
  2. 静态的方法不能存取非静态的变量。
    非静态的方法可以读取静态的变量,但不能调用该类静态的方法或变量。
  3. autoboxing功能能够自动的将primitive主数据类型转换成包装过的对象。
    如创建int的ArrayList:
public void doNumNewWay(){
    ArrayList<Integer>listOfNumbers=new ArrayList<Integer>();      //创建Integer类型的ArrayList
    listOfNumbers.add(3);     //虽然ArrayList没有add(int)方法,但编译器会自动包装
    int num=listOfNumbers.get(0);     //编译器也会自动解开Integer对象的包装,因此可直接赋值给int
}

generic类型规则是只能指定类或接口类型,因此ArrayList无法通过编译。

  1. 数字的格式化
    数字的格式化只要调用静态的String.format()并传入值与格式设定。
    String s=String.format("%,d",1000000000) //输出1,000,000,000 格式化指令中的%代表一项变量,此变量就是跟在格式化指令后面的参数。
  • “格式化说明”的格式
    %[argument number] [flags] [width] [.precision] type
    format("%,6.1f",42.000);
  • 多参数时
    参数会根据顺序应用在%上
    int one=20456654;int two=100567890.248907String s=String.format("The rank is %,d out of %,.2f", one, two); //输出:The rank is 20,456,654 out of 100,567,890.25
  • 日期时间
    从java.util.Data获取当前时间,用java.util.Calendar操作日期
    无法获取Calendar的实例(Calendar是抽象类),但可以取得它的具体子类的实例。
    Calendar cal=Calendar.getInstance(); //对静态方法的调用,返回具体子类的实例cal.set(2004,1,7,15,40); //将时间设定为2004年1月7日15:40

异常处理

1. 异常是一种Exception类型的对象,Exception类型的对象可以是任何它的子类的实例。
try{
        //把有风险的程序放在try块
    } catch(Exception ex){
        //用catch块摆放异常状况的处理程序
    }
  1. 当程序代码调用有风险的方法时,也就是声明有异常的方法,就是该方法把异常丢给你的。

    方法可以抓住其他方法抛出的异常,异常总是会丢回给调用方。
    在编写可能抛出异常的方法时,它们都必须声明有异常。方法可以用throw关键词抛出异常对象。
    throw new FileIsTooSmallException(); (1)有风险、会抛出异常的程序代码
public void takeRisk() throws BadException{  //必须声明会抛出BadException
    if (abandonAllHope){
        throw new BadException();   //创建Exception对象并抛出
    }
}
  1. (2)调用该方法的程序代码
public void crossFingers(){
    try{
        anObject,takeRisk();
    } catch (BadException ex){
      System.out.println("Aaargh!");
      ex.printStackTrace();  //若无法从异常中恢复,至少使用printStackTrace()列出有用信息
        }
  }
  1. RuntimeException被称为不检查异常,不需要声明或被包在try/catch块中。
  2. finally块用来存放不管有没有异常都得执行的程序.若try或catch块有return指令,finally还是会执行,流程会跳过finally然后再回到return指令。
    try{...} catch{...} finally{...}
  3. 方法可抛出多个异常。
    异常也能以多态的方式引用,若两个或两个以上的异常有共同的父类时,可以只声明该父类。
  • 以异常的父型来声明会抛出的异常
    public void doLaundry() throw [父类名称] { //生命成父类可以抛出任何父类的子类
  • 以所抛出的异常父型来catch异常
try{
    laundry.doLaundry();
} catch ([父类名称] cex) {
  //解决方案,可以catch父类的任何子类
}
  1. 若不同子类的处理方法不同,则分别写出catch块,若子类的处理方式相同,则可使用父类的catch来处理。
    有多个catch块时要从小排到大,沿继承树从下往上走
  2. 当调用有异常的方法,不想处理异常时可声明异常(duck掉),让调用你的方法的程序来catch该异常。
  3. 异常处理规则
  • catch与finally不能没有try
  • try与catch之间不能有程序
try{   
  x.doStuff();
}
int y=43;   //错误,不能在这里放程序
} catch(Expection ex){ }
  • try一定要有catch或finally
  • 只带有finally的try必须要声明异常
void go() throws FooException{
  try{
    x.doStuff();
  } finally{}
}

图形用户接口

  1. GUI
  • GUI从创建window开始,通常会使用JFrame。 JFrame frame=new JFrame();
  • 加入按钮、文字字段等组件。 frame.getContentPane().add(button);
  • JFrame与其他组件不同,不能直接加上组件,要使用它的cuntent pane。
  • 创建GUI的四个步骤:
  1. 创建window(JFrame)
    JFrame frame=new JFrame();
  2. 创建组件
    JButton button=new JButton("click me");
  3. 把组件加到frame上
    frame.getContentPane().add(BorderLayout.EAST,button)
  4. 显示出来
    frame.setSize(300,300);frame.setVisible(true);
  1. 事件的监听
  • 监听GUI事件接口才能知道用户对接口做了什么事。
  • Swing的GUI组件是事件的来源,事件源(如按钮)会在用户做出相关动作时(按下按钮)产生事件的对象。
  • 必须要对事件源注册所要监听的事件,事件源是一种会根据用户操作而触发事件的机制。
    对事件源注册要调用事件源的注册方法,它的方法一定是add<EventType>Listener形式。如按钮的ActionEvent注册:
    button.addActionListener(this);
  • 监听接口让事件源能够调用给你。通过实现所有的事件处理方法来实现监听接口
    public void actionPerformed(ActionEvent event) {button.setText(""clicked) }
  • 传递给事件处理方法的事件对象带有事件的信息,其中包括事件源。
  1. 图形
  • 二维图形可以直接画在图形组件上,.gif和.jpeg文件可以直接放在组件上。
  • 绘制自定义图形:创建JPanel的子类并覆盖掉paintComponent()方法
    paintComponent()方法会由GUI系统调用,不能自己调用。它的参数是不能自己创建的Graphics对象。
  • 使用Image绘制.jpg
import java.awt.*;
import javax.swing.*;

class MyDrawPanel extends JPanel{  //创建JPanel的子类
    public void paintComponent(Graphics g){ //方法由系统调用
        g.setColor(Color.orange);
        g.fillRect(20,50,100,100);  //把g想象成绘制装置,设置颜色形状
        Image image=new ImageIcon("c.jpg").getImage(); //图文件名
        g.drawImage(image,3,4,this);//图案左上角离panel左边缘像素位置
    }
}
  • paintComponent()的Graphics参数实际上是个Graphics2D的实例。
    调用Graphics2D方法前,必须把Graphics对象转换为Graphics2D。
    Graphics2D g2d = (Graphics2D) g;
  1. 多重监听——内部类
  • 内部类可以使用外部所有的方法与变量,就算私用的也一样。
  • 不是任意一个内部类都可以存取其他外部类的方法和变量。只能存取它所属的那一个。(内部类的实例一定会绑在外部类的实例上)
    创建外部类的实例→→使用外部类的实例创建内部类的实例→→内外可以交互使用变量
class MyOuter{
    private int x;   //外部私用x实例变量
    MyInner inner=new MyInner();   //创建内部实例
    public void doStuff(){
        inner.go();     //调用内部方法
    }
    class MyInner{
        void go(){
        x=42;    }  //内部可以使用外部x变量
    }   //关闭内部类
}   //关闭外部类
  • 使用内部类后,可以用不同的方法实现同一个接口方法
    x.takeButton(new DogInnerButton())

swing

  • 布局管理器会控制嵌套在其组件中组件的大小和位置。当某个组件加到背景组件上面时,被加入的组件是由背景组件的布局管理器管理的。
    Eg. 面板A的布局管理器控制面板B的大小位置,面板B的布局管理器控制三个按钮的大小位置,面板A管不到三个按钮。
  • 布局管理器在做决定之前会询问组件的理想大小,并根据策略决定采用哪些数据。
  1. 三大布局管理器
  • BorderLayout
  • 将背景组件分为东西南北中央5个区域,每个区域只能放一个组件。
    frame.getContentPane().add(BorderLayout.EAST,button);//指定区域
  • BorderLayout布局上的南北区域使用组件的理想高度而不管宽度,东西区域恰好相反,中间区域使用剩下的空间
  • FlowLayout
  • FlowLayout布局会由左至右,由上至下依次加入的顺序安置组件,若宽度超过则换行。
  • FlowLayout布局会让组件在长宽上都使用理想的尺寸大小。
  • JPanel的布局管理器默认布局为FlowLayout布局。当把面板加到框架时,面板的大小位置还是受BorderLayout布局管理,但面板内部组件(通过panel.add(button)加入)由面板的FlowLayout布局管理。
  • BoxLayout
  • BoxLayout布局垂直排列组件,组件在长宽上都使用理想尺寸大小。
  • 框架默认使用BoxLayout布局,面板默认使用FlowLayout布局。
    //改变布局管理器//它的构造函数需要知道管理哪个组件以及使用哪个轴panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));
  • 可调用setLayout()改变面板布局管理器。

序列化和文件的输入/输出

  1. 存储对象:
  • 用序列化(若只有自己写的Java程序会用到这些数据)
    将被序列化的对象写入文件,让程序去文件中读取序列化的对象并把它们展开回到状态
  • 写纯文本文件(若数据需要被其他程序引用)
    用其他程序可解析的特殊字符写入文件中,如写成用tab字符来分隔的档案
  1. 对象的序列化
  • 将序列化对象写入文件

FileOutputStream把字节写入文件。ObjectOutputStream把对象转换成可以写入串流的数据。当调用ObjectOutputStream的writeObject时,对象会被打成串流送到FileOutputStream写入文件。

  • ①. 创建FileOutputStream
    //创建存取文件的FileOutputStream对象//若文件不存在,会自动被创建出来FileOutputStream filestream=new FileOutputStream("My.ser"); ②. 创建ObjectOutputStream
    //能写入对象,但不能直接连接文件,所以需要参数指引ObjectOutputStream os=new ObjectOutputStream(fileStream); ③. 写入对象
    //将变量引用的对象序列化并写入My.ser文件os.writeObject(characterOne);os.writeObject(characterTwo);os.writeObject(characterThree); ④. 关闭ObjectOutputStream
    //关闭所关联的输出串流os.close();
  • 对象必须实现序列化接口才能被序列化。
    Serializable接口能让类序列化,即此类的对象可以通过序列化的机制来存储。如果父类实现序列化,则子类也自动实现,无论是否有明确声明。
  • 当对象被序列化时,整个对象版图都会被序列化。被该对象引用的实例变量,和所有被引用的对象也都会被序列化。
  • 序列化是全有或全无的,若有不能序列化的对象,执行期间会碰到异常。
  • 若有某个实例变量不能活不应该被序列化,就把它标记为transient,序列化程序会将它跳过。
import java.io.*;
public class Pond implements Serializable{   //Pond对象可被序列化
    transient String currentID;     //变量标记为不需要序列化
    String username;       //变量会被序列化被保存
    private Duck duck=new Duck();   //Duck实例变量
    public static void main(String[] args){
        Pond myPond=new Pond();
        try{   //可能会抛出异常
          FileOutputStream fs=new FileOutputStream("Pond.ser");
          ObjectOutputStream os=new ObjectOutputStream(fs);  
          os.writeObject(myPonds);  //将myPond序列化的同时Duck也会被序列化
          os.close();
        }  catch(Expection ex){
            ex.printStackTrace();
        }
    }
}
public class Duck{    //Duck不能被序列化!因此没有实现序列化!
    //duck程序代码
}
  1. 解序列化

对象从stream中读出,Java虚拟机通过存储的信息判断对象的class类型,尝试寻找加载对象的类。若找不到或无法加载该类,虚拟机会抛出异常。新的对象会被配置在堆上,但构造函数不会执行。

  • 读取对象的顺序必须与写入的顺序相同。
  • 若对象在继承树上有个不可序列化的祖先类,则该不可序列化类以及它之上的类的构造函数(包括可序列化)就会执行。即从第一个不可序列化的父类开始,都会重新初始状态。
  • 标为transient的变量在还原时会被标记为null或使用primitive主数据类型的默认值。
  • readObject()的返回类型是Object,因此解序列化回来的对象还需要转换成原来的类型。
  • 静态变量不会被序列化,static代表“每一个类一个”,对象被还原时,静态变量会维持类中原本的样子,而不是存储时的样子。
    ①. 创建FileOutputStream
    //若文件不存在,会抛出异常FileInputStream filestream=new FileInputStream("My.ser"); ②. 创建ObjectOutputStream
    //它知道如何读取对象,但需要链接的stream提供文件存取ObjectInputStream os=new ObjectInputStream(fileStream); ③. 读取对象
    //每次调用readObject()都会从stream中读出下一个对象Object one=os.readObject();Object Two=os.readObject();Object Three=os.readObject(); ④. 转换对象类型
    GameCharacter elf=(GameCharacter) one;GameCharacter troll=(GameCharacter) two;GameCharacter magician=(GameCharacter) three; ⑤. 关闭ObjectInputStream
    //FileInputStream会自动跟着关掉os.close();
  1. 写入文本文件
  • 用到String文件名的串流大部分都可以用File对象来代替String
  • 用FileWriter这个连接串流写入文本文件(字符串)
    fileWriter.write("My first String to save");
  • 输入/输出相关操作都必须包在try/catch块中
import java.io.*;     //加载包
class WriteAFile{
    public static void main(String[] args){
        try{
            FileWriter writer=new FileWriter("Foo.txt");
            writer.write("hello,foo!");
            writer.close();    //记得关掉
        }  catch(IOException ex){
              ex.printStackTrace();
        }
    }
}
  1. File类代表磁盘上的文件,但并不是文件中的内容。
    File对象代表磁盘上的文件或目录的路径名称,(如:/User/Kathy/Data/GameFile.txt),但它并不能读取或代表文件中的数据。
  2. File对象可创建。浏览和删除目录。
  • 创建代表现存盘文件的File对象
    File f=new File("MyCode.txt");
  • 建立新目录
    File dir=new File("Chapter7");dir.mkdir();
  • 列出目录下的内容
if (dir.isDirectory()){
    String[] dirContents=dir.list();
    for(int i=0;i<dirContents.length;i++){
        System.out.println(dirContents[i]);
    }
}
  • 取得文件或目录绝对路径
    System.out.println(dir.getAbsolutePath());
  • 删除文件或目录
    boolean isDelected=f.delect();
  1. 缓冲区
  • BufferedWriter和FileWriter链接,BufferedWriter可暂存一堆数据,待写满后再实际写入磁盘。
    BufferedWriter writer=new BufferedWriter(new FileWriter(aFile));
  • 调用writer.flush()方法可以强制缓冲区立即写入
  1. String的split()可以把字符串拆开成String的数组,其中分割字符不会被当做数据来看待。
String toTest="What is blue+yellow/green";  //从文件读出时的样式
String[] result=toTest.split("/");  //split()会用参数所指定的字符把String拆开成两个部分
.for(String token:result){
    System.out.pringln(token);   //把数组中每个元素逐一列出
}

网络与线程

  1. 客户端与服务器应用程序通过Socket连接来沟通。Socket是代表两台机器之间网络连接的对象(java.net.Socket)
  2. Socket连接的建立代表两台机器之间存有对方的信息,包括IP地址与端口号。
  3. TCP端口是一个16位宽,用来指定服务器上特定的应用程序。能够让用户连接到服务器上各种不同的应用程序。
    一个地址可以有65536个不同的端口号,从0~1023端口号是保留给HTTP/FTP/SMTP等已知的特定服务使用。
  4. 客户端通过Socket连接服务器
    Socket s=new Socket("127.0.0.1",5000) 一旦建立连接,客户端可以从Socket取得底层串流
    sock.getInputStream();
  5. 使用BufferedReader从Socket读取数据
  • 建立BUfferedReader链接InputStreamReader与来自Socket的输入串流以读取服务器的文本数据
  • InputStreamReader是个转换字节成字符的桥梁,主要用来链接BufferedReader与底层的Socket输入串流
    ①. 建立对服务器的Socket连接
    Socket chatSocket=new Socket("127.0.0.1",5000); ②. 建立连接到Socket上低层输入串流的InputStreamReader
    //从Socket取得输入串流InputStreamReader stream=new InputStreamReader(chatSocket.getInputStream()); ③. 建立BufferedReader来读取
    BufferedReader reader=new BufferedReader(stream);String message=reader.readLine();
  1. 用PrintWriter写数据到Socket上
  • 建立直接链接Socket输出串流的PrintWriter请求print()方法或println()方法来送出String给服务器
  • Printwriter是字符数据和字节之间的转换桥梁,可以衔接String和Socket两端
    ①. 对服务器建立Socket连接
    Socket chatSocket=new Socket("127.0.0.1",5000); ②. 建立连接到Socket的PrintWriter
    PrintWriter writer=new PrintWriter(chatSocket.getOutputStream()); ③. 写入数据
    writer.println("message to send");writer.print("another message");
  1. 服务器可以使用ServerSocket来等待客户端对特定端口的请求
    当ServerSocket接到请求后,它会做一个Socket连接来接受客户端的请求
    ①. 服务器应用程序对特定端口创建出ServerSocket
    //服务器应用程序开始监听来自4242端口的客户端请求ServerSocket serverSock=new ServerSocket(4242); ②. 客户端对服务器应用程序建立Socket连接
    Socket sock=new Socket("190.165.1.103",4242) ③. 服务器创建出与客户端通信的新Socket
    Socket sock=serverSock.accept();//accept()方法在等待用户的Socket连接时闲置,当用户连接时,方法会返回一个Socket在不同的端口上以便与客户端通信,ServerSocket与Socket端口不同,因此ServerSocket可空闲等待其他用户
  2. 线程是独立的线程,每个线程都有独立的执行空间。
  3. Thread是Java中用来表示线程的类,有启动线程、连接线程、让线程闲置等方法。
    每个Thread需要一个任务来执行,即线程在启动时去执行的工作,Runnable即为这个工作
  4. Runnable接口
  • Runnable是个接口,线程的任务可以定义在任何实现Runnable的类上
  • 要把Runnable传给Thread的构造函数才能启动新的线程,线程只在乎传给Thread的构造函数的参数是否为实现Runnable的类
  • Runnable接口只有一个方法:public void run(),run()是新线程执行的第一项方法
  1. 启动新的线程
    ①. 建立Runnable对象(线程的任务)
    Runnable threadJob=new Runnable(); ②. 建立Thread对象(执行工人)并赋值Runnable(任务)
    Thread myThread=new Thread(threadJob);//把Runnable对象传给Thread的构造函数,告诉Thread对象要把哪个方法放在执行空间去执行——Runnable的run()方法 ③. 启动Thread
    myThread.start();//线程在初始化以后还没有调用start()之前处于新建立的状态。调用Thread对象的start()之后,会建立出新的执行空间,处于可执行状态等待被挑出执行。
  2. 通常线程会在可执行与执行中两种状态中来回交替。
    当Java虚拟机的调度器选择某个线程之后就处于执行中状态,单处理器的机器只能有一个执行中的线程。
  3. 调度不能保证任何执行时间和顺序,不会完全平均分配执行。
  4. Thread.sleep()
  • 调用sleep()让所有的线程都有机会运行。
    Thread.sleep()静态方法可以强制线程进入等待状态。
    Thread.sleep(200); //线程休息200毫秒
  • sleep()方法可能会抛出InterruptedException异常,所以要包在try/catch块,或者把它声明出来。
  1. setName()方法可帮线程命名
  2. 线程并发性问题:两个或两个以上线程存取单一对象的数据,即两个不同执行空间上的方法在堆上对同一个对象执行getter或setter
  3. 使用synchornized关键词修饰方法,使它每次只能被单一的线程存取,防止两个线程同时进入同一对象的同一方法
    public synchronized void male(int amount) //加到方法的声明上即可
  4. 每个对象都有单一的锁,单一的钥匙。
    若对象有同步化的方法,则线程只能在取得钥匙的情况下进入线程。
    一旦某个线程进入对象的同步化方法,则其他线程无法进入该对象上的任何同步化线程。
  5. 同步化会降低程序运行效率,并可能导致锁死现象。所以原则上最好只做最少量的同步化。
    当只有个别调用需要同步化时,使用参数所指定的对象的锁来做同步化。
public viod go(){   //不需要整个同步化
    doStuff();
    synchronized(this){    //以当前对象(this)来同步化
        criticalStuff();
        moreCriticalStuff();
    }
}

集合与泛型

  1. Treeset方法可以保持有序的状态,
  2. java.util.Collection.sort() 会把list中的String依照字母排序
  3. 运用泛型可以创建出类型安全更好的集合。(让编译器能够防止把Dog对象加到Cat集合中)

没有泛型时,无法声明ArrayList的类型,,只能用Object操作,出来时回事Object类型的引用。
使用泛型后,仅有Fish对象可以放入ArrayList中,取出的还是Fish对象的引用。

  1. 泛型
  • 创建被泛型的类(例如ArrayList)的实例
    new ArrayList<Song>()
  • 声明与指定泛型类型的变量
    List<Song> songList=new ArrayList<Song>()
  • 声明(与调用)取用泛型类型的方法
    void foo(List<Song> list)x.foo(songList)
  1. 使用泛型的类:①类的生命 ②新增元素的方法的声明
    public class ArrayList<E> extends AbstractList<E> implements List<E>public boolean add(E o)//E为类型参数,代表用来创建与初始ArrayList的类型,如创建ArrayList<Dog>,则add会变为add(Dog o)
  2. 使用泛型的方法:
    ①使用定义在类声明的类型参数
    public class ArrayList<E> extends AbstractList<E> implements List<E>public boolean add(E o)//当声明类的类型参数时,可以直接把该类或接口类型用在任何地方 ②使用未定义在类声明的类型参数
    public <T extends Animal> void takeThing(ArrayList<T> list)

<T extends Animal>表示任何被声明为Animal及Animal的子类的ArrayList是合法的;
而方法参数ArrayList<Animal> list表示只有ArrayList是合法的

  1. 对泛型来说,extends适用于extends和implements,适用于类和接口。
  2. 比较器ComparableComparator
  • Comparable是个内比较器,实现了Comparable接口的类可以和自己比较,依赖于compareTo方法的实现
    public interface Comparable<T> {int compareTo(T o);} compareTo()方法会从某个T的对象来调用,然后传入其他T对象的引用。执行compareTo()方法的T必须要判别在排序位置上他自己是高于、低于或等于所传入的T。
  • Comparator是个外比较器,接口有一个compare()方法
  1. Collection API三个主要接口:List、Set、Map
  • LIST:一种知道索引位置的集合(主要对付顺序)
    List知道某物在系列集合中的位置,可以有多个元素引用相同的对象
  • SET:不允许重读的集合(注重独一无二性质)
    知道某物是否已经存在于集合中,不会有多个元素引用相同的对象
  • MAP:使用成对的键值和数据值(主要用于用key来搜索)
    Map会维护与key有关联的值。两个key可以引用相同的对象,值可以重复,但key不能重复

Map事实上并没有继承Collection这个接口,但也是Java集合的一种

  1. 对象的等价
  • 引用相等性
    引用到堆上同一个对象的两个引用是相等的

    若想知道两个引用是否相等,使用==比较变量上的字节组合
if(foo==bar){
    //两个引用都指向同一个对象
}
  • 对象相等性
    堆上的两个不同对象在意义上是相同的

    必须覆盖过hashCode()才能确保两个对象有相同的hashcode,也要确保以另一个对象为参数的equals()调用会返回true
if(foo.equals(bar)&&foo.hashCode()==bar.hashCode()){
    //两个引用指向同一个对象,或者两个对象是相等的
}
  1. HashSet检查重复:hashCode()与equals()
    如果foo与bar两对象相等,则foo.equals(bar)会返回true,且两者的hashCode()也会返回相同的值。要让Set把对象视为重复的,就必须让它们符合上面的条件被视为相同的
  2. TreeSet:防止重复并保持有序
    TreeSet的元素必须是Comparable
    要使用TreeSet,下列其中一项必须为真:
  • 集合中的元素必须是有实现Comparable的类型
class Book implements Comparable{
    String title;
    public Book(String t){
        title=t;
    }
    public int compareTo(Object b){
        Book book=(Book) b;
        return (title.compareTo(Book.title));
    }
}
  • 使用重载、取用Comparator参数的构造函数来创建TreeSet
public class BookCompare implements Comparator<Book>{
    public int compare(Book one,Book two){
        return(one.title.compareTo(two.title));
    }
}
class Test{
    public void go(){
        Book b1=new Book("Cats");
        Book b2=new Book("Remix");
        Book b1=new Book("Finding");
        BookCompare bCompare=new BookCompare();
        TreeSet<Book> tree=new TreeSet<Book>(bCompare);
        tree.add(new Book("Cats"));
        tree.add(new Book("Remix"));
        tree.add(new Book("Finding"));
        System.out.println(tree);
    }
}
  1. Map:
import java.util.*;
public class TestMap{
    public static void main(String[] args){
        HashMap<String,Integer> scores=new HashMap<String,Integer>();  //HashMap需要两个类型参数:关键字和值
        scores.put("Kathy",42);
        scores.put("Bert",343);   //使用put()取代add(),需要两个参数
        scores.put("Skyler",420);
        System.out.println(scores);
        System.out.println(scores.get("Bert"));  //get()取用关键字参数,返回它的值
    }
}

Map列出时,以{key=value,key=value,…}的形式打印出

  1. 泛型与多态
  • 若方法的参数是Animal的数组,它也能够取用Animal次类型的数组。
    即若方法声明为void foo(Animal[] a){},若Dog有extend过Animal,则可以用两种方法调用foo(anAnimalArray); , foo(aDogArray);
  • 若把方法声明成取用ArrayList,它只会取用ArrayList的参数,ArrayList和ArrayList都不行
  1. 泛型的万用字符
public void takeAnimals(ArrayList<? extends Animal>animal){
    for(Animal a:animals){
        a.eat();
    }
}

包、jar存档文件和部署

  1. 标准的组织化结构:创建项目目录,在其下建立source和classes目录。把源代码(.java)存储在source目录下。在编译时让输出(.class)产生在classes目录下。
    %cd MyProject/source%javac -d../classes MyApp.java

-d可以指定编译过的程序放在指定的目录结构中

  1. 执行程序:
    %cd MyProject/classesjava MyApp //从classes目录来运行
  2. 把程序包进JAR
    大部分完全在本机的Java应用程序都是以可执行的JAR来部署的
    可执行的JAR代表用户不需把文件抽出来就能运行

    ①确定所有的类文件都在classes目录下
    ②创建manifest.txt来描述哪个类带有main()方法
    Main-Class:MyApp //后面没有class 将此文件放在classes目录下
    ③执行jar工具来创建带有所有类以及manifest的JAR文件
    %cd MyProject/classes%jar -cvmf manifest.txt app1.jar *.class
  3. 执行JAR
    Java虚拟机能从JAR中载入类,并调用该类的main()方法
    Java虚拟机会检查JAR的manifest寻找入口,-jar标识告诉Java虚拟机所给的是个JAR
    %cd MyProject/classes%java -jar app1.jar
  4. 将类放在包中可防止名称冲突。在类名称前加上域名。
    com.headfirstjava.Chart 在程序源文件最前面加上包指令可以把类包进包中。
    package com.headfiresjava 类必须待在完全相对应于包结构的目录中才能包进包中。以上为例,Chart类必须放在com目录下的headfirstjava目录中
  5. 编译与执行包
    加上-d(directory)选项来编译:
    %cd MyProject/source%javac -d../classes/com/headfirstjava/Chart.java 执行程序:
    %cd MyProject/classes%java com.headfirstjava.Chart

-d会要求编译器将编译结果根据包的结构建立目录并输出,若classes目录下包的目录结构还没有建好,编译器会自动处理。

  1. JAR与包
    ①确定所有的类文件都在classes目录下正确对应的包结构中
    ②创建manifest.txt来描述哪个类带有main()方法,以及确认有使用完整的类名称
    Main-Class:com.headfirstjava.Chart //后面没有class 将此文件放在classes目录下
    ③执行jar工具来创建带有所有类以及manifest的JAR文件
    %cd MyProject/classes%jar -cvmf manifest.txt Chart.jar com
  2. Java Web Start
  • JWS技术让你能够从网站来部署独立的客户端程序
  • 用户通过点选网页上的某个连接来启动JWS的应用程序。一旦程序下载后,它能独立于浏览器之外来执行。
  • JWS有个必须安在客户端的helper app,用来管理下载、更新和启动JWS程序
  • JWS程序由两部分组成:可执行的JAR与.jnlp文件
  • .jnlp文件用来描述JWS应用程序的XML文件。它有tag以指定JAR的名称和位置,以及带有main()的类名称
  1. JWS工作方式:
    ①客户端点击某个网页上JWS应用程序的链接(.jnlp文件)
    ②Web服务器接受请求发出.jnlp文件(不是JAR)给客户端浏览器
    ③浏览器启动JWS,JWS的helper appa读取.jnlp文件,然后向服务器请求.jar
    ④Web服务器发送.jar文件
    ⑤JWS取得JAR并调用指定的main()来启动应用程序
  2. 创建于部署JWS的步骤
    ①将程序制作成可执行的JAR
    ②编写.jnlp文件
    ③把.jnlp与JAR文件放到Web服务器
    ④对Web服务器设定新的mime类型
    application/x-java-jnlp-file ⑤设定网页连接到.jnlp文件(.html)
    <HTML><BODY><a href="MyApp2.jnlp">Launch My Application</a></BODY></HTML>

远程部署的RMI

  1. 远程调用方法过程
    ①客户端对象对辅助设施对象调用doBigThing()
    ②客户端辅助设施把调用信息打包通过网络送到服务端的辅助设施
    ③服务端辅助设施解开来自客户端辅助设施的信息,并以此调用真正的服务
  • 客户端对象以为它调用的是远程的服务,因为辅助设施假装成该服务对象
  • 客户端辅助设施假装成服务,但只是“代理”
  • 服务器辅助设施会通过Socket连接来自客户端设施的要求,解析打包送来的信息,调用真正的服务
  1. 在RMI中,客户端辅助设施称为stub,服务端辅助设施称为skeleton
    stub是个处理底层网络细节的辅助性对象,会把方法的调用包装起来送到服务器上
  2. 创建远程服务
  • 步骤1:创建Remote接口
    远程接口定义了客户端可以远程调用的方法,是个作为服务的多态化类,stub和服务都会实现此接口。
import java.rmi.*;
//Remote是标记性接口,没有方法,你的接口必须继承java.rmi.Remote
public interface MyRemote extends Remote{
    //所有接口中的方法都必须声明RemoteException
    //确定参数和返回值都是primitive主数据类型或Serializable
    public String sayHello() throws RemoteException;    
}
  • 步骤2:实现Remote
    这是真正执行的类,它实现出定义在该接口上的方法,是客户端会调用的对象。
import java.rmi.*;
import java.rmi.server.*;
//服务必须要实现Remote——就是客户端会调用的方法
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
    //编译器会确保你实现出所有接口定义的方法,此例中只有一个
    //无需声明RemoteException
    public String sayHello(){
        return "Server says,'Hey'";
    }
    //父类UnicastRemoteObject的构造函数声明了异常,因此必须写出构造函数,代表你的构造函数会调用带有风险的程序代码
    public MyRemoteImpl() throws RemoteException{}
    public static void main(String[] args){
        try{
            //帮服务器命名,客户端会靠名字查询registry
            //向registry注册服务,RMI系统会把stub加到registry中
            //使用java.rmi.Naming的rebind()来注册远程服务
            MyRemote service=new MyRemoteImpl(); //创建远程对象
            Naming.rebind("Remote Hello",service);
        }  catch(Exception ex){
              ex.printStackTrace();
        }
    }
}
  • 步骤3:用rmic产生stub与skeleton
    对实现出的类(不是remote接口)执行rmic,客户端和服务器都有helper,无需创建类的源代码,在执行JDK所附的rmic工具时会自动处理掉。
  • 步骤4:启动RMI registry
    调用命令行,确定从可以存取到该类的目录来启动。
    %rmiregistry
  • 步骤5:启动远程服务
    调用另一个命令行,可能是从你所实现的类上的main()或独立的启动用类来进行。
    %java MyRemoteImpl
  • 客户端
import java.rmi.*;  //Naming类在java.rmi中
public class MyRemoteClient{
    public static void main(String[] args){
        new MyRemoteClient().go();
    }
    public void go(){
        try{
            //类型转换,客户端必须要使用与服务相同的类型,必须要转换成接口类型,因为查询结果是Object类型
            //rmi://主机名称或IP地址/注册名称
            MyRemote service=(MyRemote) Naming.lookup("rmi://127.0.0.1/Remote Hello");
            String s=service.sayHello();
            System.out.println(s);
        } catch(Exception ex){
            ex.printStackTrace();
        }
    }
}

使用RMI时注意:

  • RMI registry必须在同一台机器上与远程服务一起执行
  • 使用Naming.rebind()注册服务前rmiregistry必须启动
  • 参数和返回类型必须做成可序列化
  1. servlet
  • servlet是放在HTTP Web服务器上面运行的Java程序
  • servlet用来处理与用户交互的网页程序。如用户提交一些信息给服务器,servlet可以处理信息并把特定结果以网页形式返回给用户
  • 创建并执行servlet
  • 在网页服务器找到可以存放servlet的地方,servlet必须放在特定位置才能执行
  • 取得servlet.jar并添加到classpath上

servlet不是Java标准函数库的一部分,需要servlet.jar文件中的servlet相关包才能编译出servlet。可从java.sun.com或Web服务器供应商处获得(Java2 Enterprise Edition,即J2EE带有Servlet函数库)

  • 通过extend过HttpServlet编写servlet的类
    public class MyServletA extends HttpServlet {...}
  • 编写HTML调用servlet
    用户点击引用到servlet网页链接时,服务器找到servlet并根据HTTP的GET、POST调用方法。
    <a href="servlets/MyServletA">This is a servlet.</a>
  • 给服务器设定HTML网页和servlet
    必须要有支持servlet的Web服务器才能运行servlet,如apache.org的Tomcat
import java.io.*;
import java.servlet.*;
import java.servlet.http.*;  //两个包需另行下载,不是Java标准库的一部分
public class MyServletA extends HttpServlet{
    //servlet可以通过doGet()的相应参数取得输出串流来组成响应的网页
    public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{
        //告诉服务器和浏览器相应结果的形式
        response.setContentType("text/html");
        //此对象会给我们可写回结果的输出串流
        PrintWriter out=response.getWriter();
        //print对象是个网页,内容由程序的输出组成
        String message="If you're reading this,it worked";
        out.println("<HTML><BODY>");
        out.println("<H1>"+message+"</H1>");
        out.println("</BODY></HTML>");
        out.closse();
    }
}
  • J2EE服务器包括Web服务器和Enterprise JavaBeans(EJB)服务器。
  • EJB服务器具有一组你只靠RMI不会有的服务,比如交易管理、安全性、并发性、数据库和网络功能等
  • EJB服务器作用于RMI调用和服务层之间
  • EJB对象会拦截对带有商业逻辑的bean的调用,并将所有EJB服务器提供的服务加上一个中介层。
  • bean对象被保护不受客户端直接存取,只有服务器能够与bean沟通,使服务器能介入执行安全性和交易控管等工作
  1. Jini也使用RMI,多了几个关键功能:
    (1)自适应探索(adaptive discovery)
    (2)自恢复网络(self-healing networks)

当服务上线,它会动态探索网络上的Jini查询服务并申请注册,服务会送出一个序列化的对象给查询服务,且注册的是所实现的接口。取得查询服务的引用后,客户端可以查询“有没有东西实现ScientificCalculator”,查询服务若找到,会返回该服务所放上来的序列化对象。