自己平时长期积累的java资料可供大家学习

java 中数据转换

1、如何将字符串String转化为整数int

   int i = Integer.parseInt(str);
   int i = Integer.valueOf(my_str).intValue();

    注: 字串转成Double, Float, Long的方法大同小异。

2、如何将字符串String转化为Integer

    Integer integer=Integer.valueOf(i)

3、如何将整数 int 转换成字串 String?


答:有三种方法:

   String s = String.valueOf(i);
   String s = Integer.toString(i);
   String s = "" + i;

注:Double, Float, Long 转成字串的方法大同小异。

4、如何将整数int转化为Integer

   Integer integer=new Integer(i)

5、如何将Integer转化为字符串String

    Integer integer=String()

6、如何将Integer转化为int

    int num=Integer.intValue()


7、如何将String转化为BigDecimal


    BigDecimal d_id=new BigDecimal(str)

 
response设置文件类型

  contentType

/**不可识别文件:“application/octet-stream”
             * bmp:"application/x-bmp"
             * doc:"application/msword"
             * exe:"application/x-msdownload"
             * jpg:"image/jpeg"
             * mdb:"application/msaccess"
             * mp3:"audio/mp3"
             * pdf:“application/pdf”
             * ppt:"application/vnd.ms-powerpoint"
             * rm:"application/vnd.rn-realmedia"
             * rmvb:"application/vnd.rn-realmedia-vbr"
             * swf:"application/x-shockwave-flash"
             * xls:"application/vnd.ms-excel"
             * */

 
java中this的用法

                    java 中的this关键字的使用
在java程序中this关键字随处可见,用处也比较多;或许很多人对this理解不够清楚。下面我将结合例子程序总结一下this的用法。 主要分为四个小部分。


第一、调用类中的属性(this.属性)。


package com.javawxs;   
 
public class PersonInformation {   
    String name;   
    String address;   
    int age;   
 
    PersonInformation(String name, String address, int age) {   
        this.name = name;   
        this.address = address;   
        this.age = age;   
    }   
}   
 
在PersonInformation()方法中,可以直接访问对象的成员变量,但是在同一范围内,不允许定义两个名字相同的局部变量,那么就需要一种方法将成员变量和跟她同名字的方法的参数区分出来,这时候就可以使用this;在上边的例子程序里this在方法体中指向引用当前方法的对象实例,用this.属性来调用类中的属性。最为常见的就是在setter()和getter()中的使用。


第二、调用类中的方法(this.方法())。


package com.javawxs;   
 
import java.util.Random;   
 
public class PersonInformation {   
 
    int i = new Random().nextInt(200);// 产生一个随机数   
 
    public int getI() {   
        return i;   
    }   
 
    public boolean greaterthan100() {   
        if (this.getI() > 100) {   
            return true;   
        }   
        return false;   
    }   
在greaterthan100()方法中通过this.getI()来调用当前方法的对象实例中的getI()方法,当然这个例子中的this也可以省略。


第三、表示当前对象:this。


class A {   
    public A() {   
        new B(this);   
    }   
}   
 
class B {   
    A a;   
 
    public B(A a) {   
        this.a = a;   
    }   
}   
这个小程序中,B的构造方法的参数为A的实例;在A中创建B的实例时将this作为A的的实例传给B的构造方法。new B(this)相当于new B(new A()) 。


第四、调用当前类中的构造方法:this()。


在构造方法使用this(参数表)就会调用同一个类中的另一个相对应的构造方法。以下是一个很典型的例子程序。


package com.javawxs;   
 
public class Tester {   
    int var;   
 
    // 构造方法一   
    Tester() {   
        System.out.println(“good-bye”);   
    }   
 
    // 构造方法二   
    Tester(String s) {   
        this();   
        System.out.println(s);   
    }   
 
    // 构造方法三   
    Tester(int var) {   
        this(“hello”);   
    }   
 
    // 构造方法四   
    Tester(double var) {   
        this.var = (int) var;   
    }   
 
    public static void main(String[] args) {   
        Tester t = new Tester(5);   
    }   
}   
 


以上程序的执行结果是:


                            good-bye


                             hello


          在构造方法中使用this(参数表)还需要注意三个问题:


1、错误示例:

Tester(String s) {   
        System.out.println(s);   
        this();   
           
    }   
调用另一个构造函数的语句必须在最起始的位置。


2、错误示例:


public void mytest(){   
     this(“hello”);   
 }   
不能在构造函数以外的任何函数内调用构造函数。


3、错误示例:

Tester(String s) {   
        this();   
        this(“hello”);   
        System.out.println(s);   
    }   
在一个构造函数内只能调用一个构造函数。

 
toString() parse() value()的区别

1.parse()是SimpleDateFomat里面的方法,
你说的应该是parseInt()或parsefloat()这种方法吧,顾名思义
比如说parseInt()就是把String类型转化为int类型。如 String a= "123";
      int b = Integer.parseInt(a);这样b就等于123了。
2.ValueOf()方法比如说 Integer.valueOf() 是把String类型转化为
Integer类型(注意:是Integer类型,而不是int类型,
int类型是表示数字的简单类型,Integer类型是一个引用的复杂类型)如:
String a= "123";Integer c =Integer.valueOf(a);
//Integer类型可以用intValue方法转化为int类型int b =c.intValue();
这时候这个b就等于123了3. toString()
可以把一个引用类型转化为String字符串类型。
下面举个例子与2相反,把Integer转化为String类型:
Integer a = new Integer(123);String b =a.toString()
;这时候b就是 "123" 了

 
多线程基础

多线程基础知识去评论?
时间:2012-12-15  分类:Java SE  标签:MLDN, 多线程  
在多进程的处理中,在同一个时间段上有多个程序运行,但在同一个时间点上只能有一个程序运行。Java本身属于多线程的语言,提供了线程的处理机制。


1、线程实现的两种方式
在Java中有两种方式实现多线程,一是继承Thread类,二是实现Runnable接口。



1、Thread类
Thread类在java.lang包中定义


一个类只要继承了Thread类,同时覆写了本类中的run()方法,则就可以实现多线程的操作。


package com.javawxs.threaddemo;   
public class MyThread extends Thread {   
    private String name;   
    public MyThread(String name) {   
        this.name = name;   
    }   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            System.out.println(“Thread运行:” + name + this.name + “,i=” + i);   
        }   
    }   
}   
package org.wxs.threaddemo;   
public class ThreadDemo01 {   
    public static void main(String[] args) {   
        MyThread m1 = new MyThread(“线程1″);   
        MyThread m2 = new MyThread(“线程2″);   
        m1.run();   
        m2.run();   
    }   
}   
       此时,先执行m1,再执行m2;没有实现交互的运行。


从JDK的文档可以发现,调用start()方法,则会通过JVM找到run()方法。


package com.javawxs.threaddemo;   
public class ThreadDemo01 {   
    public static void main(String[] args) {   
        MyThread m1 = new MyThread(“线程1″);   
        MyThread m2 = new MyThread(“线程2″);   
        m1.start();   
        m2.start();   
    }   
}   
       此时,程序已经可以正常地交互运行。


使用start()方法启动线程的原因:


在JDK的安装路径下,src.zip是全部的java源程序,通过此代码找到Thread类中的start()方法的定义(JDK1.7版本):


public synchronized void start() {   
        if (threadStatus != 0)   
            throw new IllegalThreadStateException();   
        group.add(this);   
        boolean started = false;   
        try {   
            start0();   
            started = true;   
        } finally {   
            try {   
                if (!started) {   
                    group.threadStartFailed(this);   
                }   
            } catch (Throwable ignore) {   
            }   
        }   
    }   
    private native void start0();   
       多线程要进行CPU资源抢占,即要等待CPU调度,这些调度都是由各个操作系统的底层实现,所以在Java程序中没有办法实现,所以Java的设计者定义了native关键字,使用此关键字表示可以调用操作系统的底层函数,即JNI技术(Java Native Interface)。


2、Runnable接口
在实际开发中一个多线程操作很少用Thread类完成,而是通过Runnable接口完成。


Runnable接口的定义:


public interface Runnable{   
   public void run();   
}   
所以,一个类只要实现了此接口,并覆写run()方法。
package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    private String name;   
    public MyThread(String name) {   
        this.name = name;   
    }   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            System.out.println(“Thread运行:” + name + this.name + “,i=” + i);   
        }   
    }   
}   
       在使用Runnable定义的子类中没有start()方法,而只有Thread类才有start()类中才有。


在thread类中存在以下构造方法:


public Thread(Runnable target)  
此构造方法接收runnable的子类实例,也就是可以通过Thread类启动Runnable实现多线程。
package com.javawxs.threaddemo;   
public class RunnableDemo01 {   
    public static void main(String[] args) {   
        MyThread m1 = new MyThread(“线程1″);   
        MyThread m2 = new MyThread(“线程2″);   
        new Thread(m1).start();   
        new Thread(m2).start();   
    }   
}   
3、两种实现的区别
在程序的开发中要实现多线程最好实现Runnable接口,实现Runnable接口相对继承Thread类有如下好处:


·避免单继承的局限,一个类可以同时实现多个接口


·适合于资源的共享


以卖票为例,通过Thread类完成。


package com.javawxs.threaddemo;   
public class MyThread extends Thread{   private int ticket = 5;// 一共5张票   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            if (this.ticket > 0) {   
                System.out.println(“卖票:ticket =” + this.ticket–);   
            }   
        }   
    }   
}   
       建立三个线程,同时卖票。


package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread m1 = new MyThread();   
        MyThread m2 = new MyThread();   
        MyThread m3 = new MyThread();   
        m1.start();   
        m2.start();   
        m3.start();   
    }   
}   
       发现一共卖了15张票,但实际只有5张票,所以每个线程在卖自己的票,不能达到资源共享的目的。


下面通过实现Runnable接口来完成。可以实现资源共享。


package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    private int ticket = 5;// 一共5张票   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            if (this.ticket > 0) {   
                System.out.println(“卖票:ticket =” + this.ticket–);   
            }   
        }   
    }   
}   
建立三个线程,同时卖票。


package org.wxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread m = new MyThread();   
        new Thread(m).start();   
        new Thread(m).start();   
        new Thread(m).start();   
    }   
}   
三个线程一共卖出5张票,Runnable实现的多线程可以达到资源共享的目的。


Thread类和Runnable接口的联系:


public class Thread extends Object implements Runnable  
Thread是Runnable的子类。
2、线程的操作方法
对于线程所有的操作都在Thread类中定义的。


1、设置和取得名字
在Thread类中存在以下几个方法可以设置和取得名字:


·设置名字:


|-set:public final void setName(String name)


|-构造:


|-public Thread(Runnable target String name)


|-public Thread(String name)


·取得名字:


|-public final String getName()


在线程的操作中提供了一个方法,取得当前操作线程:


public static Thread current Thread()


对于线程的名字一般是在启动前进行设置。


package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            System.out.println(Thread.currentThread().getName() + “正在运行”);   
        }   
    }   
}   
       测试是否取得了名字。


package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread m = new MyThread();   
        Thread t1 = new Thread(m, “线程1″);   
        Thread t2 = new Thread(m, “线程2″);   
        Thread t3 = new Thread(m, “线程3″);   
        t1.start();   
        t2.start();   
        t3.start();   
    }   
}   
       看以下代码:

package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread m = new MyThread();   
        Thread t1 = new Thread(m, “线程1″);   
        t1.start();   
        m.run();   
    }   
}   
 
在程序运行时主方法就是一个主线程。


2、线程的休眠


在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)


·public static void sleep(long millis, int nanos) throws InterruptedException

package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    public void run() {   
        for (int i = 0; i < 100; i++) {   
            try {   
                Thread.sleep(500);   
            } catch (InterruptedException e) {   
                e.printStackTrace();   
            }   
            System.out.println(Thread.currentThread().getName() + “正在运行”);   
        }   
    }   
}   
       主程序:

package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread m = new MyThread();   
        Thread t1 = new Thread(m, “线程1″);   
        Thread t2 = new Thread(m, “线程2″);   
        t1.start();   
        t2.start();   
    }   
}   
3、线程的中断
在sleep()方法存在InterruptException,造成此异常的方法就是中断:


·public void interrupt()

package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    public void run() {   
        System.out.println(“1、进入run()方法体”);   
        try {   
            System.out.println(“2、线程休眠20秒钟”);   
            Thread.sleep(20000); // 每个休眠20秒   
            System.out.println(“3、线程正常休眠20秒钟”);   
        } catch (InterruptedException e) {   
            System.out.println(“4、线程休眠被中断”);   
            return;// 返回方法调用处   
        }   
        System.out.println(“5、正常结束run()方法体”);   
    }   
}   
 

package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread mt = new MyThread();   
        Thread thread = new Thread(mt, “线程A”);   
        thread.start();// 启动线程   
        try {   
            Thread.sleep(2000); // 保证程序至少执行2秒   
        } catch (InterruptedException e) {   
            e.printStackTrace();   
        }   
        thread.interrupt(); // 中断   
    }   
}   
4、线程的优先级
使用以下方法设置优先级:


·public final void setPriority(int newPriority)


线程中有3种优先级:


·最高:public static final int MAX_PRIORITY


·默认:public static final int NORM_PRIORITY


·最低:public static final int MAX_PRIORITY


package com.javawxs.threaddemo;   
public class MyThread implements Runnable {   
    public void run() {   
        for (int i = 0; i < 20; i++) {   
            System.out.println(Thread.currentThread().getName() + “正在运行”);   
        }   
    }   
}   
&nbsp:


package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        MyThread mt = new MyThread();   
        Thread thread1 = new Thread(mt, “线程A”);   
        Thread thread2 = new Thread(mt, “线程B”);   
        Thread thread3 = new Thread(mt, “线程C”);   
        thread3.setPriority(Thread.MAX_PRIORITY);   
        thread2.setPriority(Thread.NORM_PRIORITY);   
        thread1.setPriority(Thread.MIN_PRIORITY);   
        thread1.start();   
        thread2.start();   
        thread3.start();   
    }   
}
       取得优先级:


package com.javawxs.threaddemo;   
public class Test {   
    public static void main(String[] args) {   
        System.out.println(Thread.currentThread().getPriority());   
        System.out.println(“MIN_PRIORITY = ” + Thread.MIN_PRIORITY);   
        System.out.println(“MAX_PRIORITY = ” + Thread.MAX_PRIORITY);   
        System.out.println(“NOM_PRIORITY = ” + Thread.NORM_PRIORITY);   
    }   
}   
从运行结果发现:


5


MIN_PRIORITY = 1


MAX_PRIORITY = 10


NOM_PRIORITY = 5

 
枚举基础

枚举基础去评论?
时间:2012-12-28  分类:Java SE  标签:MLDN  
1、枚举的作用
在C语言中存在一个枚举类型,通过此类型可以限制一个内容的取值范围,但在JDK1.5之前,java并不存在枚举类型,所以很多之前已经习惯使用枚举的开发人员感到不适应,为了解决该问题,在JDK1.5之后加入了枚举类型


2、 enum关键字
在JDK1.5之后,可以直接使用enum关键字定义一个枚举的类型。


public enum Color {   
    RED, GREEN, BLUE;   
}   
此时已经完成了一个枚举类型的定义,那么在使用的时候直接通过
public class TestColor {   
    public static void main(String[] args) {   
        Color c = Color.BLUE;   
        System.out.println(c);   
    }   
}   
而且枚举在JDK1.5之后也可以直接在switch语句中使用,switch判断的时候只能判断字符或数字,但是在JDK1.5之后却可以判断枚举类型


public class TestColor {   
    public static void main(String[] args) {   
        switch (Color.RED) {   
        case RED: {   
            System.out.println(“红色”);   
            break;   
        }   
        case GREEN: {   
            System.out.println(“绿色”);   
            break;   
        }   
        case BLUE: {   
            System.out.println(“蓝色”);   
            break;   
        }   
        }   
    }   
}   
还可以通过方法得到全部的枚举的取值范围


public class TestColor {   
    public static void main(String[] args) {   
        for (Color c : Color.values()) {   
            System.out.print(c + “、”);   
        }   
    }   
}   
3、Enum和enum关键字的区别
使用enum关键字可以定义一个枚举类型,那么实际就相当于定义了一个类,此类继承了Enum类而已。


在Enum类的构造方法中是受保护的,实际上对于每一个枚举对象一旦声明之后,就表示自动调用此构造方法;所有的编号采用自动编号的方式进行。


public class TestColor {   
    public static void main(String[] args) {   
        for (Color c : Color.values()) {   
            System.out.println(c.name() + “->” + c.ordinal());   
        }   
    }   
}   
还可以通过此类找到枚举中的一个指定的内容使用valueOf()方法。


public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)  
传入的时候直接传入Color.class即可。


public class TestColor {   
    public static void main(String[] args) {   
        Color c = Color.valueOf(Color.class, “RED”);   
        System.out.println(c);   
    }   
}   
通过以上代码可以发现,直接通过枚举取得指定内容更加简单。


4、 类集对枚举的支持
在JDK1.5之后,增加了两个类集的操作类:EnumSet、EnumMAp。


1、EnumMap
EnumMap是Map接口的子类,操作形式与Map一致。


import java.util.EnumMap;   
import java.util.Map;   
public class TestColor {   
    public static void main(String[] args) {   
        EnumMap<Color, String> eMap = new EnumMap<Color,String>(Color.class);   
        eMap.put(Color.RED, “红”);   
        eMap.put(Color.BLUE, “蓝”);   
        eMap.put(Color.GREEN, “绿”);   
        for (Map.Entry<Color, String> me : eMap.entrySet()) {   
            System.out.println(me.getKey() + “ ” + me.getValue());   
        }   
    }   
}   
2、EnumSet
EnumSet本身是Set接口的子类,但是在此类中并没有任何的构造方法定义,表示构造方法私有化了,此类的方法都是静态的操作。

import java.util.EnumSet;   
import java.util.Iterator;   
public class EnumSetDemo {   
    public static void main(String[] args) {   
        EnumSet<Color> eSet = EnumSet.allOf(Color.class);   
        Iterator<Color> it = eSet.iterator();   
        while (it.hasNext()) {   
            System.out.println(it.next());   
        }   
    }   
}   
以上是将全部的内容设置到集合中,也可以设置一个指定类型的空集合。


import java.util.EnumSet;   
import java.util.Iterator;   
public class EnumSetDemo {   
    public static void main(String[] args) {   
        EnumSet<Color> eSet = EnumSet.noneOf(Color.class);   
        Iterator<Color> it = eSet.iterator();   
        while (it.hasNext()) {   
            System.out.println(it.next());   
        }   
    }   
5、枚举深入
 

1定义构造方法
枚举的使用非常灵活,可以在枚举中直接定义构造方法,但是一旦构造方法定义之后,则所有的枚举对象都必须明确的调用此构造方法。


public enum Color {   
    RED(“红”), GREEN(“绿”), BLUE(“蓝”);   
    private String name;   
    private Color(String name) {   
        this.name = name;   
    }   
    public String getName() {   
        return name;   
    }   
    public void setName(String name) {   
        this.name = name;   
    }   
}   
枚举中的构造不能声明为public,以为外部是不能调用枚举的构造方法的。


此时每个枚举对象都有了自己的name属性


public class TestColor {   
    public static void main(String[] args) {   
        for (Color c : Color.values()) {   
            System.out.println(c.ordinal() + “–>” + c.name() + “:”  
                    + c.getName());   
        }   
    }   
}   
2、让枚举实现一个接口
一个枚举实现一个接口之后,各个枚举对象必须分别实现接口中定义的抽象方法。


public interface Info {   
    public String getName();   
}   
下面使用枚举实现接口


public enum Color implements Info {   
    RED {   
        public String getName() {   
            return “红色”;   
        }   
    },   
    GREEN {   
        public String getName() {   
            return “绿色”;   
        }   
    },   
    BLUE {   
        public String getName() {   
            return “蓝色”;   
        }   
    }   
}   
通过代码进行测试


public class TestColor {   
    public static void main(String[] args) {   
        for (Color c : Color.values()) {   
            System.out.println(c.ordinal() + “–>” + c.name() + “:”  
                    + c.getName());   
        }   
    }   
}   
3、在枚举中定义抽象方法
在一个枚举中可以定义多个抽象方法,枚举中的每个对象都必须单独实现测方法。


public enum Color  {   
    RED {   
        public String getColor() {   
            return “红色”;   
        }   
    },   
    GREEN {   
        public String getColor() {   
            return “绿色”;   
        }   
    },   
    BLUE {   
        public String getColor() {   
            return “蓝色”;   
        }   
    };   
    public abstract String getColor();   
}  

 
排序

插入排序
插入排序是最简单的排序算法;它由N-1趟排序组成。对于p-1到N-1趟,插入排序保证从位置0到位置p的元素已经完成排序。在第p趟,将位置p的元素向左移动,直到它在前p+1个元素中的正确位置被找到的地方。


位置p上的元素储存于tmp,而(在位置p之前)所有更大的元素都被向右移动一个位置,然后tmp被置于正确的位置上;以下为代码:


package com.javawxs;   
 
public class InsertionSort {   
    public static void main(String[] args) {   
        Integer[] arr = { 71, 45, 32, 76, 142, 43, 98, 54, 14, 333, 12, 31, 3 };   
        insertionSort(arr);   
        for (int i = 0; i < arr.length; i++)   
            System.out.print(arr[i] + “ ”);   
 
    }   
 
    public static <T extends Comparable<? super T>> void insertionSort(T[] a) {   
        int j;   
        for (int p = 1; p < a.length; p++) {   
            T tmp = a[p];   
            for (j = p; j > 0 && tmp.compareTo(a[j - 1]) < 0; j–)   
                a[j] = a[j - 1];   
            a[j] = tmp;   
        }   
    }   
}   
 

希尔排序
希尔排序(Shellsort)的名字源于的它的发明者Donald Shell,它通过比较相距一定间隔的元素来实现;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟为止;所以希尔排序也叫缩减增量排序。


希尔排序使用一个序列h1,h2,h3……,ht,叫做增量序列,只要h1=1,那么任何增量序列都是可行的。在使用hk的一趟排序之后,对于每个i,都有a[i]<=a[i+hk];所有间隔hk的元素都被排序。


希尔排序的一个重要性质:一个hk排序的文件保持它的hk排序性;实际上,假如不这样的话,那么希尔排序也就没什么意义了,因为前面各趟排序会被后面的各趟排序打乱。


hk排序的一般做法是,对于hk,hk+1,……,N-1中的每一个位置i,把其上的元素放到i,i-hk,i-2hk,……中的正确位置上。虽然不影响最终结果,但是通过观察可以发现一趟hk的排序作用就是对hk个独立的子数组执行一次插入排序。


以下为代码:


package com.javawxs;   
 
public class ShellSort {   
    public static void main(String[] args) {   
        Integer[] arr = { 71, 45, 32, 76, 142, 43, 98, 54, 14, 333, 12, 31, 3 };   
        shellSort(arr);   
        for (int i = 0; i < arr.length; i++)   
            System.out.print(arr[i] + “ ”);   
 
    }   
 
    public static <T extends Comparable<? super T>> void shellSort(T[] a) {   
        int j;   
        for (int gap = a.length / 2; gap > 0; gap /= 2)   
            for (int i = gap; i < a.length; i++) {   
                T tmp = a[i];   
                for (j = i; j >= gap && tmp.compareTo(a[j - gap]) < 0; j -= gap)   
                    a[j] = a[j - gap];   
                a[j] = tmp;   
            }   
    }   
}   
堆排序
首先建立N个元素的二叉堆,花费时间(O(N)),然后执行N次的deleteMin操作。按照顺序,最小的元素先离开堆。通过将这些元素记录到第二个数组然后再将数组拷贝回来,得到N个元素的排序。由于每个deleteMin花费时间O(log N),因此运行总时间为O(N log N)。


该算法的主要问题在于它使用了一个附加数组,增大大储存需求;可以这样来避免使用第二个数组:在每次deleteMin之后,堆缩小1;所以位于堆中最后的单元正好可以存放刚才删除的元素;最后一次deleteMin后,正好得到数组的递减排序。要想得到递增排序通过改变有序的特性使父亲的关键字的值大于儿子的关键字的值;就得到了(max)堆。


第一步以线性时间建立一个堆。然后通过每次将堆中的最后元素与第一个元素交换,执行N-1次deleteMin操作,每次将堆的大小缩减1并进行下滤。结束时,数组排序完成。


以下为代码:

package com.javawxs;   
 
public class HeapSort {   
    private static int leftChild(int i) {   
        return 2 * i + 1;   
    }   
 
    private static <T extends Comparable<? super T>> void perDown(T[] a, int i,   
            int n) {   
        int child;   
        T tmp;   
        for (tmp = a[i]; leftChild(i) < n; i = child) {   
            child = leftChild(i);   
            if (child != n - 1 && a[child].compareTo(a[child + 1]) < 0)   
                child++;   
            if (tmp.compareTo(a[child]) < 0)   
                a[i] = a[child];   
            else  
                break;   
        }   
 
        a[i] = tmp;   
    }   
 
    private static <T extends Comparable<? super T>> void swapReferences(T[] a,   
            int i, int n) {   
        T temp = a[i];   
        a[i] = a[n];   
        a[n] = temp;   
    }   
 
    public static <T extends Comparable<? super T>> void heapSort(T[] a) {   
        for (int i = a.length / 2; i >= 0; i–)   
            perDown(a, i, a.length);   
        for (int i = a.length - 1; i > 0; i–) {   
            swapReferences(a, 0, i);   
            perDown(a, 0, i);   
        }   
 
    }   
 
}
前两篇谈了插入排序、希尔排序、堆排序和归并排序,都还是比较容易掌握的,这篇将主要说说快速排序算法;因为快速排序比较复杂涉及的算法较多,精炼而且高度优化的内部循环;使得快速排序运行非常迅速,但也加大了理解难度。它的平均运行时间是O(N log N),最坏情形是O(N^2);不过这种情况很少出现。通过快速排序和堆排序的结合使用,我们可以基本上所有的输入都达到快速排序的快速运行时间。和归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法由以下步骤组成:


1、  如果S中元素个数是0或1,则返回。


2、  取S中任一元素v,作为枢纽元。


3、  将S-{v}(S中其余元素)分为两个不相交集合:S1={x∈S-{v}|x<=v}和S2={x∈S-{v}|x>=v}.


4、  返回{quicksort(S1)后跟v,继而返回quicksort(S2)}。


由于对那些等于枢纽元的元素的处理上,第三步分割的描述不是唯一的,因此这就成为了一种设计决策。我们最好能有效的解决这个问题;我们希望把等于枢纽元的大约一半的关键字分到S1中,剩余的分到S2中,就像二叉查找树那样保持平衡。


关于选择枢纽元的选取问题:


·选择第一个元素作为枢纽元,如果输入的随机的,那么不会存在问题,但是如果输入是预排序的或者是反序,就会出现严重的问题,所有元素不是被分到S1就是都被分到S2中,更严重的是这种情况会出现在所有的递归调用中。例如,在一个预排序的输入中,把第一个元素作为枢纽元,那么快速排序花费的时间是二次的,可实际毫无意义。所以选取第一个 元素作为枢纽元是非常不可取的。


·随机选择枢纽元,这种方法比较安全,但是生成随机数会开销很大,会影响算法的运行时间。


·选择数组中的中值(第[N/2]个最大数);不过,这很难算出并且会明显减慢排序速度。一般情况下是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。使用三数中值分割法消除了预排序输入的最坏情形,并减少了比较次数。


分割策略:例如输入为9,2,5,10,7,4,6,3,8,1的数据,第一步通过将枢纽元与最后的元素交换使其离开被分割的数据段,i从第一个元素开始,j从倒数第二个元素开始。下图为当前状态:


在分割阶段就是要把所有小于枢纽元的元素放到数组左边,把大于枢纽元的元素放到数组右边。


当i在j左边的时候,我们将i右移过所有小于枢纽元的元素,并将j左移,移过大于枢纽元的元素。当i和j停止时,i指向一个大元素而j指向一个小元素。如果i在j的左边,那么两个元素互换,把一个大元素推到右边而把一个小元素推到左边在上面的例子中,i不移动而j划过一个位置,如下图:


然后交换i和j指向的元素,重复该过程直到i和j彼此交错为止。


现在i和j已经交错,不用再交换,最后将枢纽元与i指向的元素交换。


在最后一步当枢纽元与i所指向的元素交换时,在p<i的每一个元素必然都是小元素,在p>i上的元素必然都是大元素。


还有一个重要细节必须考虑-如何处理那些等于枢纽元的元素。当i或j遇到一个等于枢纽元的元素时是否应该停止移动。i和j应该做同时移动,否则分割将会向一个方向偏;例如可能出现所有的元素都被划分到S2中。


现在假设数组中所有元素都相等,如果i和j都停止,那么相等元素间会有多次无意义的交换,通过归并排序分析,这时运行时间为O(N log N)。


如果i和j都不停止,那必须防止i和j越界,不再进行交换操作。也许不错,但是要把枢纽元交换到i最后到过的位置,这个位置是倒数第二个位置(或者最后的位置)。这样将会产生两个非常不均衡的子数组,如果所有元素都相同,那么花费的时间为O(N^2)。对于预排序的输入来说,与使用第一个元素作为枢纽元没有区别。


所以,进行不必要的交换建立两个均衡的子数组比得到两个不均衡子数组好得多。但i和j遇到等于枢纽元的关键字时,i和j停止,从而避免了话费二次时间。


对于很小的数组(N<=20),快速排序的性能相对较低,倒不如使用插入排序。



以下是程序代码:



package com.javawxs;   
 
public class QuickSort {   
    public static final int CUTOFF = 3;// 当截取元素数量大于此值是使用快速排序   
 
    public static <T extends Comparable<? super T>> void quickSort(T[] a) {   
        quickSort(a, 0, a.length - 1);   
    }   
 
    private static <T extends Comparable<? super T>> T median3(T[] a, int left,   
            int right) {   
        int center = (left + right) / 2;   
        if (a[center].compareTo(a[left]) < 0) {   
            swapReferences(a, left, center);   
        }   
        if (a[right].compareTo(a[left]) < 0) {   
            swapReferences(a, left, right);   
        }   
        if (a[right].compareTo(a[center]) < 0) {   
            swapReferences(a, center, right);   
        }   
 
        swapReferences(a, center, right - 1);   
        return a[right - 1];   
    }   
 
    private static <T extends Comparable<? super T>> void quickSort(T[] a,   
            int left, int right) {   
        if (left + CUTOFF <= right) {   
            T pivot = median3(a, left, right);   
            int i = left, j = right - 1;   
            for (;;) {   
                while (a[i].compareTo(pivot) < 0)   
                    i++;   
                while (a[j].compareTo(pivot) > 0)   
                    j–;   
                if (i < j) {   
                    swapReferences(a, i, j);   
 
                } else {   
                    break;   
                }   
 
            }   
 
            swapReferences(a, i, right - 1);   
            quickSort(a, left, i - 1);   
            quickSort(a, i + 1, right);   
 
        } else {   
            insertionSort(a, left, right);   
        }   
 
    }   
 
    private static <T extends Comparable<? super T>> void swapReferences(T[] a,   
            int i, int n) {   
        T temp = a[i];   
        a[i] = a[n];   
        a[n] = temp;   
    }   
 
}   
 
数组的增删改查

虽然数组是由固定容量创建的,但是在需要的时候可以用双倍的容量创建一个不同的数组。解决了由于使用数组而产生的问题-需要对数组的大小进行估计;下面通过代码程序演示数组在必要的时候进行扩充,并实现“添加”、“删除”、“查找”等操作。


代码:


package com.javawxs;   
 
public class MyArrayList<T> {   
    private T[] object;   
    private int length;// 长度   
    private int capacity;// 容量   
    private static final int BASE_LENGTH = 10;// 初始长度   
 
    public MyArrayList() {   
        this(BASE_LENGTH);   
    }   
 
    // 初始长度的构造   
    public MyArrayList(int intLength) {   
        if (intLength > 0) {   
            this.length = intLength;   
            this.capacity = intLength;   
            object = (T[]) new Object[intLength];   
        } else {   
            this.length = BASE_LENGTH;   
            this.capacity = BASE_LENGTH;   
            object = (T[]) new Object[BASE_LENGTH];   
        }   
    }   
 
    // 添加元素   
    public boolean add(T anEntry) {   
        try {   
            if (length == capacity) {   
                copyEntry();   
            }   
            object[length] = anEntry;   
            length++;   
            return true;   
        } catch (Exception e) {   
            return false;   
        }   
    }   
 
    private void copyEntry() {   
        T[] newEntry = object;   
        this.capacity *= 2;   
        object = (T[]) new Object[this.capacity];   
        for (int index = 0; index < newEntry.length; ++index) {   
            object[index] = newEntry[index];   
        }   
    }   
 
    // 指定位置添加元素   
    public boolean add(int index, T anEntry) {   
        try {   
            while (index >= capacity) {   
                copyEntry();   
            }   
            if (index < length) {   
                moveEntry(index, anEntry);   
                length++;   
                return true;   
            } else {   
                object[index] = anEntry;   
                int sub = index - length + 1;   
                length += sub;   
                return true;   
            }   
        } catch (Exception e) {   
            return false;   
        }   
    }   
 
    // 当插入位置在length之前时移动元素   
    private boolean moveEntry(int aPosition, T anEntry) {   
        if (aPosition < length) {   
            for (int index = length - 1; index >= aPosition; ++index) {   
                object[index + 1] = object[index];   
            }   
            object[aPosition] = anEntry;   
            return true;   
        }   
        return false;   
    }   
 
    public boolean contains(T anEntry) {   
        for (T t : object) {   
            if (t.equals(anEntry)) {   
                return true;   
            }   
        }   
        return false;   
    }   
 
    public T get(int index) {   
        if (index < length) {   
            return object[index];   
        }   
        return null;   
    }   
 
    public boolean isEmpty() {   
        return (length == 0);   
    }   
 
    public boolean isFull() {   
 
        return (length == capacity);   
 
    }   
 
    // 移除元素   
    public T remove(int index) {   
 
        if (index < length) {   
            T newEntry = object[index];   
            for (int i = index; i < length; ++i) {   
                object[i] = object[i + 1];   
            }   
            length–;   
            return newEntry;   
        }   
        return null;   
    }   
 
    // 替换元素   
    public T replace(int index, T anEntry) {   
        if (index < length) {   
            T newEntry = object[index];   
            object[index] = anEntry;   
            return newEntry;   
        }   
        return null;   
    }   
 
    public int getLength() {   
        return length;   
    }   
 
    public int getCapacity() {   
        return this.capacity;   
    }   
 
    public void show() {   
        for (int index = 0; index < length; ++index) {   
            if (object[index] != null) {   
                System.out.println(index + “:” + object[index].toString());   
            } else {   
                System.out.println(index + “:” + “null”);   
            }   
        }   
    }   
}  

 
序列化

一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才是可序列化的。因此如果要序列化某些类的对象,这些类就必须实现Serializable接口。而实际上,Serializable是一个空接口,没有什么具体内容,它的目的只是简单的标识一个类的对象可以被序列化。
追问
那进行序列化有什么好处呢?
回答
什么情况下需要序列化
a)当你想把的内存中的对象写入到硬盘的时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
再稍微解释一下:a)比如说你的内存不够用了,那计算机就要将内存里面的一部分对象暂时的保存到硬盘中,等到要用的时候再读入到内存中,硬盘的那部分存储空间就是所谓的虚拟内存。在比如过你要将某个特定的对象保存到文件中,我隔几天在把它拿出来用,那么这时候就要实现Serializable接口;
b)在进行java的Socket编程的时候,你有时候可能要传输某一类的对象,那么也就要实现Serializable接口;最常见的你传输一个字符串,它是JDK里面的类,也实现了Serializable接口,所以可以在网络上传输。
c)如果要通过远程的方法调用(RMI)去调用一个远程对象的方法,如在计算机A中调用另一台计算机B的对象的方法,那么你需要通过JNDI服务获取计算机B目标对象的引用,将对象从B传送到A,就需要实现序列化接口。
没有人说的话能全部准确,批判性的参考。
提问者评价
thank you !
评论(128)|437


longturong |五级采纳率44%
擅长:编程语言数据库DBJAVA相关
按默认排序|按时间排序
其他1条回答
2011-09-16 20:05ymiqplgao|七级
序列化。主要解决就是完成如何将JVM中的对象保存起来,当需要这个对象的时候,再把它读入内存。为了实现这个功能,便有这个序列化的方法。
当然有序列化,就有反序列化。见于你不明白,详细的也就不多说了,参考相关资料先了解一下


举个例子,你编写了一款游戏,保存记录时把所有状态一一保存非常麻烦,这时就可以使用Serializable(序列化接口),它的作用是可以将一个对象实例序列化,序列化后你可以选择将它保存在你需要的位置。相对的,读取后生成的对象所有属性(除了设置为瞬时值的属性)将和原对象的属性相同(只是内存地址不同)。这样可以方便的将一个java对象写入到磁盘中,保存该对象的所有状态!值得注意的是序列化的对象中包含的属性和其他对象都需要实现序列化接口,不然无法正常序列化!在hibernate里,并非所有的实体类必须实现序列化接口,因为在hibernate中我们通常是将基本类型的数值映射为数据库中的字段。而基础类型都实现了序列化接口(String也实现了)。所以,只有在想将一个对象完整存进数据库(存储为二进制码),而不是将对象的属性分别存进数据库,读取时再重新构建的话,就可以不用实现序列化接口。

 
正则表达式

1. 引子
  目前,正则表达式已经在很多软件中得到广泛的应用,包括*nix(Linux, Unix等),HP等操作系统,PHP,C#,Java等开发环境,以及很多的应用软件中,都可以看到正则表达式的影子。


  正则表达式的使用,可以通过简单的办法来实现强大的功能。为了简单有效而又不失强大,造成了正则表达式代码的难度较大,学习起来也不是很容易,所以需要付出一些努力才行,入门之后参照一定的参考,使用起来还是比较简单有效的。


例子: ^.+@.+\\..+$


  这样的代码曾经多次把我自己给吓退过。可能很多人也是被这样的代码给吓跑的吧。继续阅读本文将让你也可以自由应用这样的代码。


  注意:这里的第7部分跟前面的内容看起来似乎有些重复,目的是把前面表格里的部分重新描述了一次,目的是让这些内容更容易理解。

上一页    下一页
2. 正则表达式的历史


  正则表达式的“祖先”可以一直上溯至对人类神经系统如何工作的早期研究。Warren McCulloch 和 Walter Pitts 这两位神经生理学家研究出一种数学方式来描述这些神经网络。
  1956 年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为“神经网事件的表示法”的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为“正则集的代数”的表达式,因此采用“正则表达式”这个术语。


  随后,发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究,Ken Thompson 是 Unix 的主要发明人。正则表达式的第一个实用应用程序就是 Unix 中的 qed 编辑器。


  如他们所说,剩下的就是众所周知的历史了。从那时起直至现在正则表达式都是基于文本的编辑器和搜索工具中的一个重要部分。


3. 正则表达式定义
  正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。


列目录时, dir *.txt或ls *.txt中的*.txt就不是一个正则表达式,因为这里*与正则式的*的含义是不同的。
  正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为元字符)组成的文字模式。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。


3.1 普通字符
  由所有那些未显式指定为元字符的打印和非打印字符组成。这包括所有的大写和小写字母字符,所有数字,所有标点符号以及一些符号。


3.2 非打印字符 字符  含义
\cx  匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\f  匹配一个换页符。等价于 \x0c 和 \cL。
\n  匹配一个换行符。等价于 \x0a 和 \cJ。
\r  匹配一个回车符。等价于 \x0d 和 \cM。
\s  匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S  匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t  匹配一个制表符。等价于 \x09 和 \cI。
\v  匹配一个垂直制表符。等价于 \x0b 和 \cK。


3.3 特殊字符


   所谓特殊字符,就是一些有特殊含义的字符,如上面说的"*.txt"中的*,简单的说就是表示任何字符串的意思。如果要查找文件名中有*的文件,则需要对*进行转义,即在其前加一个\。ls \*.txt。正则表达式有以下特殊字符。


特别字符 说明
$ 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 '\n' 或 '\r'。要匹配 $ 字符本身,请使用 \$。
( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 和


* 匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+ 匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
. 匹配除换行符 \n之外的任何单字符。要匹配 .,请使用 \。
[  标记一个中括号表达式的开始。要匹配 [,请使用 \[。
? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \?。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\\' 匹配 "\",而 '\(' 则匹配 "("。
^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。要匹配 ^ 字符本身,请使用 \^。
{ 标记限定符表达式的开始。要匹配 {,请使用 \{。
| 指明两项之间的一个选择。要匹配 |,请使用 \|。


  构造正则表达式的方法和创建数学表达式的方法一样。也就是用多种元字符与操作符将小的表达式结合在一起来创建更大的表达式。正则表达式的组件可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。
 


3.4 限定符


   限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有*或+或?或{n}或{n,}或{n,m}共6种。
*、+和?限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个?就可以实现非贪婪或最小匹配。
   正则表达式的限定符有:
 


字符  描述
*  匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
+  匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
?  匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
{n}  n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,}  n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m}  m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。


3.5 定位符


   用来描述字符串或单词的边界,^和$分别指字符串的开始与结束,\b描述单词的前或后边界,\B表示非单词边界。不能对定位符使用限定符。


3.6 选择


   用圆括号将所有选择项括起来,相邻的选择项之间用|分隔。但用圆括号会有一个副作用,是相关的匹配会被缓存,此时可用?:放在第一个选项前来消除这种副作用。
   其中?:是非捕获元之一,还有两个非捕获元是?=和?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串。


3.7 后向引用


   对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左至右所遇到的内容存储。存储子匹配的缓冲区编号从 1 开始,连续编号直至最大 99 个子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
   可以使用非捕获元字符 '?:', '?=', or '?!' 来忽略对相关匹配的保存。



上一页    下一页


4. 各种操作符的运算优先级


   相同优先级的从左到右进行运算,不同优先级的运算先高后低。各种操作符的优先级从高到低如下:
 


操作符  描述
\  转义符
(), (?:), (?=), []  圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}  限定符
^, $, \anymetacharacter  位置和顺序
|  “或”操作


上一页    下一页


5. 全部符号解释


字符  描述
\  将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,'n' 匹配字符 "n"。'\n' 匹配一个换行符。序列 '\\' 匹配 "\" 而 "\(" 则匹配 "("。
^  匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。
$  匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。
*  匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
+  匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
?  匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
{n}  n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,}  n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m}  m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。
?  当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。
.  匹配除 "\n" 之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,请使用象 '[.\n]' 的模式。
(pattern)  匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 '\(' 或 '\)'。
(?:pattern)  匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 "或" 字符 (|) 来组合一个模式的各个部分是很有用。例如, 'industr(?:y|ies) 就是一个比 'industry|industries' 更简略的表达式。
(?=pattern)  正向预查,在任何匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,'Windows (?=95|98|NT|2000)' 能匹配 "Windows 2000" 中的 "Windows" ,但不能匹配 "Windows 3.1" 中的 "Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)  负向预查,在任何不匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如'Windows (?!95|98|NT|2000)' 能匹配 "Windows 3.1" 中的 "Windows",但不能匹配 "Windows 2000" 中的 "Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
x|y  匹配 x 或 y。例如,'z|food' 能匹配 "z" 或 "food"。'(z|f)ood' 则匹配 "zood" 或 "food"。
[xyz]  字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。
[^xyz]  负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 "plain" 中的'p'。
[a-z]  字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 'a' 到 'z' 范围内的任意小写字母字符。
[^a-z]  负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]' 可以匹配任何不在 'a' 到 'z' 范围内的任意字符。
\b  匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
\B  匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
\cx  匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\d  匹配一个数字字符。等价于 [0-9]。
\D  匹配一个非数字字符。等价于 [^0-9]。
\f  匹配一个换页符。等价于 \x0c 和 \cL。
\n  匹配一个换行符。等价于 \x0a 和 \cJ。
\r  匹配一个回车符。等价于 \x0d 和 \cM。
\s  匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S  匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t  匹配一个制表符。等价于 \x09 和 \cI。
\v  匹配一个垂直制表符。等价于 \x0b 和 \cK。
\w  匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
\W  匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。
\xn  匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。.
\num  匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)\1' 匹配两个连续的相同字符。
\n  标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
\nm  标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
\nml  如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
\un  匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。



上一页    下一页


6. 部分例子


正则表达式 说明
/\b([a-z]+) \1\b/gi 一个单词连续出现的位置
/(\w+):\/\/([^/:]+)(:\d*)?([^# ]*)/  将一个URL解析为协议、域、端口及相对路径
/^(?:Chapter|Section) [1-9][0-9]{0,1}$/ 定位章节的位置
/[-a-z]/ A至z共26个字母再加一个-号。
/ter\b/ 可匹配chapter,而不能terminal
/\Bapt/ 可匹配chapter,而不能aptitude
/Windows(?=95 |98 |NT )/ 可匹配Windows95或Windows98或WindowsNT,当找到一个匹配后,从Windows后面开始进行下一次的检索匹配。



上一页    下一页
7. 正则表达式匹配规则
7.1 基本模式匹配


   一切从最基本的开始。模式,是正规表达式最基本的元素,它们是一组描述字符串特征的字符。模式可以很简单,由普通的字符串组成,也可以非常复杂,往往用特殊的字符表示一个范围内的字符、重复出现,或表示上下文。例如:


^once


  这个模式包含一个特殊的字符^,表示该模式只匹配那些以once开头的字符串。例如该模式与字符串"once upon a time"匹配,与"There once was a man from NewYork"不匹配。正如如^符号表示开头一样,$符号用来匹配那些以给定模式结尾的字符串。


bucket$


  这个模式与"Who kept all of this cash in a bucket"匹配,与"buckets"不匹配。字符^和$同时使用时,表示精确匹配(字符串与模式一样)。例如:


^bucket$


  只匹配字符串"bucket"。如果一个模式不包括^和$,那么它与任何包含该模式的字符串匹配。例如:模式


once


与字符串


There once was a man from NewYork
Who kept all of his cash in a bucket.


是匹配的。


   在该模式中的字母(o-n-c-e)是字面的字符,也就是说,他们表示该字母本身,数字也是一样的。其他一些稍微复杂的字符,如标点符号和白字符(空格、制表符等),要用到转义序列。所有的转义序列都用反斜杠(\)打头。制表符的转义序列是:\t。所以如果我们要检测一个字符串是否以制表符开头,可以用这个模式:


^\t


类似的,用\n表示“新行”,\r表示回车。其他的特殊符号,可以用在前面加上反斜杠,如反斜杠本身用\\表示,句号.用\.表示,以此类推。


7.2 字符簇


在INTERNET的程序中,正规表达式通常用来验证用户的输入。当用户提交一个FORM以后,要判断输入的电话号码、地址、EMAIL地址、信用卡号码等是否有效,用普通的基于字面的字符是不够的。


所以要用一种更自由的描述我们要的模式的办法,它就是字符簇。要建立一个表示所有元音字符的字符簇,就把所有的元音字符放在一个方括号里:


[AaEeIiOoUu]


这个模式与任何元音字符匹配,但只能表示一个字符。用连字号可以表示一个字符的范围,如:


[a-z] //匹配所有的小写字母
[A-Z] //匹配所有的大写字母
[a-zA-Z] //匹配所有的字母
[0-9] //匹配所有的数字
[0-9\.\-] //匹配所有的数字,句号和减号
[ \f\r\t\n] //匹配所有的白字符


同样的,这些也只表示一个字符,这是一个非常重要的。如果要匹配一个由一个小写字母和一位数字组成的字符串,比如"z2"、"t6"或"g7",但不是"ab2"、"r2d3" 或"b52"的话,用这个模式:


^[a-z][0-9]$


尽管[a-z]代表26个字母的范围,但在这里它只能与第一个字符是小写字母的字符串匹配。


前面曾经提到^表示字符串的开头,但它还有另外一个含义。当在一组方括号里使用^是,它表示“非”或“排除”的意思,常常用来剔除某个字符。还用前面的例子,我们要求第一个字符不能是数字:


^[^0-9][0-9]$


这个模式与"&5"、"g7"及"-2"是匹配的,但与"12"、"66"是不匹配的。下面是几个排除特定字符的例子:


[^a-z] //除了小写字母以外的所有字符
[^\\\/\^] //除了(\)(/)(^)之外的所有字符
[^\"\'] //除了双引号(")和单引号(')之外的所有字符


特殊字符"." (点,句号)在正规表达式中用来表示除了“新行”之外的所有字符。所以模式"^.5$"与任何两个字符的、以数字5结尾和以其他非“新行”字符开头的字符串匹配。模式"."可以匹配任何字符串,除了空串和只包括一个“新行”的字符串。


PHP的正规表达式有一些内置的通用字符簇,列表如下:


字符簇 含义
[[:alpha:]] 任何字母
[[:digit:]] 任何数字
[[:alnum:]] 任何字母和数字
[[:space:]] 任何白字符
[[:upper:]] 任何大写字母
[[:lower:]] 任何小写字母
[[:punct:]] 任何标点符号
[[:xdigit:]] 任何16进制的数字,相当于[0-9a-fA-F]


7.3 确定重复出现


到现在为止,你已经知道如何去匹配一个字母或数字,但更多的情况下,可能要匹配一个单词或一组数字。一个单词有若干个字母组成,一组数字有若干个单数组成。跟在字符或字符簇后面的花括号({})用来确定前面的内容的重复出现的次数。


字符簇 含义
^[a-zA-Z_]$ 所有的字母和下划线
^[[:alpha:]]{3}$ 所有的3个字母的单词
^a$ 字母a
^a{4}$ aaaa
^a{2,4}$ aa,aaa或aaaa
^a{1,3}$ a,aa或aaa
^a{2,}$ 包含多于两个a的字符串
^a{2,} 如:aardvark和aaab,但apple不行
a{2,} 如:baad和aaa,但Nantucket不行
\t{2} 两个制表符
.{2} 所有的两个字符


这些例子描述了花括号的三种不同的用法。一个数字,{x}的意思是“前面的字符或字符簇只出现x次”;一个数字加逗号,{x,}的意思是“前面的内容出现x或更多的次数”;两个用逗号分隔的数字,{x,y}表示“前面的内容至少出现x次,但不超过y次”。我们可以把模式扩展到更多的单词或数字:


^[a-zA-Z0-9_]{1,}$ //所有包含一个以上的字母、数字或下划线的字符串
^[0-9]{1,}$ //所有的正数
^\-{0,1}[0-9]{1,}$ //所有的整数
^\-{0,1}[0-9]{0,}\.{0,1}[0-9]{0,}$ //所有的小数


最后一个例子不太好理解,是吗?这么看吧:与所有以一个可选的负号(\-{0,1})开头(^)、跟着0个或更多的数字([0-9]{0,})、和一个可选的小数点(\.{0,1})再跟上0个或多个数字([0-9]{0,}),并且没有其他任何东西($)。下面你将知道能够使用的更为简单的方法。


特殊字符"?"与{0,1}是相等的,它们都代表着:“0个或1个前面的内容”或“前面的内容是可选的”。所以刚才的例子可以简化为:


^\-?[0-9]{0,}\.?[0-9]{0,}$


特殊字符"*"与{0,}是相等的,它们都代表着“0个或多个前面的内容”。最后,字符"+"与 {1,}是相等的,表示“1个或多个前面的内容”,所以上面的4个例子可以写成:


^[a-zA-Z0-9_]+$ //所有包含一个以上的字母、数字或下划线的字符串
^[0-9]+$ //所有的正数
^\-?[0-9]+$ //所有的整数
^\-?[0-9]*\.?[0-9]*$ //所有的小数


当然这并不能从技术上降低正规表达式的复杂性,但可以使它们更容易阅读。
 
字符编码

今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料。


结果,这个问题比我想象的复杂,从午饭后一直看到晚上9点,才算初步搞清楚。


下面就是我的笔记,主要用来整理自己的思路。但是,我尽量试图写得通俗易懂,希望能对其他朋友有用。毕竟,字符编码是计算机技术的基石,想要熟练使用计算机,就必须懂得一点字符编码的知识。


1. ASCII码


我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。


上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。


ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。


2、非ASCII编码


英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。


但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (?),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。


至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。


中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。


3.Unicode


正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。


可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。


Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。


4. Unicode的问题


需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。


比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。


这里就有两个严重的问题,第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。


它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。


5.UTF-8


互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。


UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。


UTF-8的编码规则很简单,只有二条:


1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。


2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。


下表总结了编码规则,字母x表示可用编码的位。


Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


下面,还是以汉字“严”为例,演示如何实现UTF-8编码。


已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“1110010010111000 10100101”,转换成十六进制就是E4B8A5。


6. Unicode与UTF-8之间的转换


通过上一节的例子,可以看到“严”的Unicode码是4E25,UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。


在Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击“文件”菜单中的“另存为”命令,会跳出一个对话框,在最底部有一个“编码”的下拉条。


里面有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。


1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。


2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。


3)Unicode big endian编码与上一个选项相对应。我在下一节会解释little endian和big endian的涵义。


4)UTF-8编码,也就是上一节谈到的编码方法。


选择完”编码方式“后,点击”保存“按钮,文件的编码方式就立刻转换好了。


7. Little endian和Big endian


上一节已经提到,Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。


这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。


因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。


那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?


Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。


如果一个文本文件的头两个字节是FEFF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。


8. 实例


下面,举一个实例。


打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。


然后,用文本编辑软件UltraEdit中的”十六进制功能“,观察该文件的内部编码方式。


1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是采用大头方式存储的。


2)Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25。


3)Unicode big endian:编码是四个字节“FE FF4E 25”,其中“FE FF”表明是大头方式存储。


4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的具体编码,它的存储顺序与编码顺序是一致的。


9. 延伸阅读


* The AbsoluteMinimum Every Software Developer Absolutely, Positively Must Know About Unicodeand Character Sets(关于字符集的最基本知识)


* 谈谈Unicode编码


* RFC3629:UTF-8, a transformation format ofISO 10646(如果实现UTF-8的规定)


关于URL编码


一、问题的由来


URL就是网址,只要上网,就一定会用到。


一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。比如,世界上有英文字母的网址“http://www.abc.com”,但是没有希腊字母的网址“http://www.aβγ.com”(读作阿尔法-贝塔-伽玛.com)。这是因为网络标准RFC 1738做了硬性规定:


"...Only alphanumerics [0-9a-zA-Z], the special characters"$-_.+!*'()," [not including the quotes - ed], and reservedcharacters used for their reserved purposes may be used unencoded within aURL."


“只有字母和数字[0-9a-zA-Z]、一些特殊符号“$-_.+!*'(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。”


这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致“URL编码”成为了一个混乱的领域。


下面就让我们看看,“URL编码”到底有多混乱。我会依次分析四种不同的情况,在每一种情况中,浏览器的URL编码方法都不一样。把它们的差异解释清楚之后,我再说如何用Javascript找到一个统一的编码方法。


二、情况1:网址路径中包含汉字


打开IE(我用的是8.0版),输入网址“http://zh.wikipedia.org/wiki/春节”。注意,“春节”这两个字此时是网址路径的一部分。


查看HTTP请求的头信息,会发现IE实际查询的网址是“http://zh.wikipedia.org/wiki/%E6%98%A5%E8%8A%82”。也就是说,IE自动将“春节”编码成了“%E6%98%A5%E8%8A%82”。


我们知道,“春”和“节”的utf-8编码分别是“E6 98A5”和“E8 8A 82”,因此,“%E6%98%A5%E8%8A%82”就是按照顺序,在每个字节前加上%而得到的。(具体的转码方法,请参考我写的《字符编码笔记》。)


在Firefox中测试,也得到了同样的结果。所以,结论1就是,网址路径的编码,用的是utf-8编码。


三、情况2:查询字符串包含汉字


在IE中输入网址“http://www.baidu.com/s?wd=春节”。注意,“春节”这两个字此时属于查询字符串,不属于网址路径,不要与情况1混淆。


查看HTTP请求的头信息,会发现IE将“春节”转化成了一个乱码。


切换到十六进制方式,才能清楚地看到,“春节”被转成了“B4 BA BDDA”。


我们知道,“春”和“节”的GB2312编码(我的操作系统“WindowsXP”中文版的默认编码)分别是“B4 BA”和“BD DA”。因此,IE实际上就是将查询字符串,以GB2312编码的格式发送出去。


Firefox的处理方法,略有不同。它发送的HTTP Head是“wd=%B4%BA%BD%DA”。也就是说,同样采用GB2312编码,但是在每个字节前加上了%。


所以,结论2就是,查询字符串的编码,用的是操作系统的默认编码。


四、情况3:Get方法生成的URL包含汉字


前面说的是直接输入网址的情况,但是更常见的情况是,在已打开的网页上,直接用Get或Post方法发出HTTP请求。


根据台湾中兴大学吕瑞麟老师的试验,这时的编码方法由网页的编码决定,也就是由HTML源码中字符集的设定决定。


  <meta http-equiv="Content-Type"content="text/html;charset=xxxx">


如果上面这一行最后的charset是UTF-8,则URL就以UTF-8编码;如果是GB2312,URL就以GB2312编码。


举例来说,百度是GB2312编码,Google是UTF-8编码。因此,从它们的搜索框中搜索同一个词“春节”,生成的查询字符串是不一样的。


百度生成的是%B4%BA%BD%DA,这是GB2312编码。


Google生成的是%E6%98%A5%E8%8A%82,这是UTF-8编码。


所以,结论3就是,GET和POST方法的编码,用的是网页的编码。


五、情况4:Ajax调用的URL包含汉字


前面三种情况都是由浏览器发出HTTP请求,最后一种情况则是由Javascript生成HTTP请求,也就是Ajax调用。还是根据吕瑞麟老师的文章,在这种情况下,IE和Firefox的处理方式完全不一样。


举例来说,有这样两行代码:


  url = url + "?q=" +document.myform.elements[0].value; // 假定用户在表单中提交的值是“春节”这两个字


  http_request.open('GET', url, true);


那么,无论网页使用什么字符集,IE传送给服务器的总是“q=%B4%BA%BD%DA”,而Firefox传送给服务器的总是“q=%E6%98%A5%E8%8A%82”。也就是说,在Ajax调用中,IE总是采用GB2312编码(操作系统的默认编码),而Firefox总是采用utf-8编码。这就是我们的结论4。


六、Javascript函数:escape()


好了,到此为止,四种情况都说完了。


假定前面你都看懂了,那么此时你应该会感到很头痛。因为,实在太混乱了。不同的操作系统、不同的浏览器、不同的网页字符集,将导致完全不同的编码结果。如果程序员要把每一种结果都考虑进去,是不是太恐怖了?有没有办法,能够保证客户端只用一种编码方法向服务器发出请求?


回答是有的,就是使用Javascript先对URL编码,然后再向服务器提交,不要给浏览器插手的机会。因为Javascript的输出总是一致的,所以就保证了服务器得到的数据是格式统一的。


Javascript语言用于编码的函数,一共有三个,最古老的一个就是escape()。虽然这个函数现在已经不提倡使用了,但是由于历史原因,很多地方还在使用它,所以有必要先从它讲起。


实际上,escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode编码值。比如“春节”的返回结果是%u6625%u8282,也就是说在Unicode字符集中,“春”是第6625个(十六进制)字符,“节”是第8282个(十六进制)字符。


它的具体规则是,除了ASCII字母、数字、标点符号“@ * _ + - . /”以外,对其他所有字符进行编码。在\u0000到\u00ff之间的符号被转成%xx的形式,其余符号被转成%uxxxx的形式。对应的解码函数是unescape()。


所以,“Hello World”的escape()编码就是“Hello%20World”。因为空格的Unicode值是20(十六进制)。


还有两个地方需要注意。


首先,无论网页的原始编码是什么,一旦被Javascript编码,就都变为unicode字符。也就是说,Javascipt函数的输入和输出,默认都是Unicode字符。这一点对下面两个函数也适用。


其次,escape()不对“+”编码。但是我们知道,网页在提交表单的时候,如果有空格,则会被转化为+字符。服务器处理数据的时候,会把+号处理成空格。所以,使用的时候要小心。


七、Javascript函数:encodeURI()


encodeURI()是Javascript中真正用来对URL编码的函数。


它着眼于对整个URL进行编码,因此除了常见的符号以外,对其他一些在网址中有特殊含义的符号“; / ?: @ & = + $ , #”,也不进行编码。编码后,它输出符号的utf-8形式,并且在每个字节前加上%。


它对应的解码函数是decodeURI()。


需要注意的是,它不对单引号'编码。


八、Javascript函数:encodeURIComponent()


最后一个Javascript编码函数是encodeURIComponent()。与encodeURI()的区别是,它用于对URL的组成部分进行个别编码,而不用于对整个URL进行编码。


因此,“; / ? : @ & = + $ , #”,这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码。至于具体的编码方法,两者是一样。


它对应的解码函数是decodeURIComponent()。




 

 

 作者:chen.yu
深信服三年半工作经验,目前就职游戏厂商,希望能和大家交流和学习,
微信公众号:编程入门到秃头 或扫描下面二维码
零基础入门进阶人工智能(链接)