2009年12月23日星期三

[note]Java中if与? :的比较

本次测试比较了if与?:表达式的效率。

        Long start = System.currentTimeMillis();
        String s = "1";
        for (int i=1;i<1000000000;i++) {
              if (s.equals("1"))
                  s = "2";       //11710,11672,11678
              s = (s.equals("1"))? "2":s; //11147,11137,11269
              s = (s.equals("1"))? "2":"1";//9539,9528,9647
        }
        Long end = System.currentTimeMillis();
        System.out.println(end - start);

 注释是三次测试所花时间,由此得到:
  • 赋值常量比赋值变量要快;
  • if所花的时间比赋值所花的时间要多;

[note]synchronized关键字

1、synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;

3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

2009年12月22日星期二

[转帖][精品]String in Java

转自:http://hxraid.javaeye.com/blog/522167

作者:Java标准类库有几千个类,唯独String不太一样。为什么这么说?就因为每次上网冲杯Java时,都能看到关于String无休无止的争论。还是觉得有必要让这个讨厌又很可爱的String美眉,赤裸裸的站在我们这些Java色狼面前了。嘿嘿....

众所周知,String是由字符组成的串,在程序中使用频率很高。Java中的String是一个类,而并非基本数据类型。 不过她却不是普通的类哦!!!

 

【镜头1】 String对象的创建
   1、关于类对象的创建,很普通的一种方式就是利用构造器,String类也不例外:
           String s=new String("Hello world");
   问题:参数"Hello world"是什么东西,也是字符串对象吗?莫非用字符串对象创建一个字符串对象???

   2、当然,String类对象还有一种大家都很喜欢的创建方式:
           String s="Hello world";
   问题:有点怪呀,怎么与基本数据类型的赋值操作(int i=1)很像呀???

在开始解释这些问题之前,我们先引入一些必要的知识:
(1) Java class文件结构
       我们都知道,Java程序要运行,首先需要编译器将源代码文件编译成字节码文件(也就是.class文件)。然后在由JVM解释执行。
       class文件是8位字节的二进制流 。这些二进制流的涵义由一些紧凑的有意义的项 组成。比如class字节流中最开始的4个字节组成的项叫做魔数 (magic),其意义在于分辨class文件(值为0xCAFEBABE)与非class文件。class字节流大致结构如下图左侧。

                               

      其中,在class文件中有一个非常重要的项——常量池 。这个常量池专门放置源代码中的常量信息(并且不同的常量存放在不同标志的常量表中)。如上图右侧是HelloWorld代码中的常量表 (HelloWorld代码如下),其中有四个不同类型的常量表(四个不同的常量池入口)。关于常量池的具体细节,请参照《深入Java虚拟机》第二版第 6章。

Java代码
  1. public class HelloWorld{  
  2.     void hello(){  
  3.         System.out.println("Hello world");  
  4.     }  
  5. }  

      显然,HelloWorld代码中的"Hello world"被编译之后,可以清楚的看到存放在了class二进制流的常量池项中(上图右侧红框区域)。并且我们还发现常量池中专门有为String类型设置的常量表 。也就是说,在编译阶段,就已经将代码中的这种("****")形式作为了字符串常量存放在常量池中了 ,这一点和下面代码中出现的整形常量(142),浮点型常量(12.1)等的处理是没有区别的。

                   String s="Hello world";
                    int intData=142;
                    double dblData=12.1;

(2) Java虚拟机运行class文件
      当Java虚拟机需要运行一个class文件时,它首先会用类装载器装载进class文件。当然也就需要在内存中存放许多东西。比如class的二进制字 节码。还有需要存储class文件中得到的其他信息,比如程序创建的对象,传递给方法的参数,返回值,局部变量等等。怎么多麻烦的数据当然需要管 理,JVM会把这些东西都组织到几个“运行时数据区 ”中。这些数据区中就有我们动不动就谈到的"堆"呀,"栈"呀什么的?想要详细了解这部分东西可以看《深入Java虚拟机》第二版第5章。
   
      在这里我只谈谈“方法区 ”这个运行时数据区。在Java虚拟机中,关于被装载类型的信息会在一个逻辑上被称为"方法区"的内存中,当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入该文件。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。
   
      方法区中的这些类型信息是很有用的,比如:这个类型的全限定名(net.single.HelloWorld);这个类型的直接超类的全限定名;这个类型 是类类型还是接口类型;这个类型的访问修饰符(public,final,static)等。还有两个大家都很熟悉的引用:指向类ClassLoader 的引用和指向Class类的引用。这是Java反射机制能够运行的关键所在。这里我们要提到的是一个非常重要的信息——该类型的常量池

    上面提到的,class文件结构中的常量池二进制流就被JVM存储在方法区中进行管理。当程序运行时需要使用到常量值的时候,直接在方法区常量池所在的内存中寻找就可以了。

(3) 操作码助忆符指令集
     将String s=new String("Hello world");编译成class文件后的指令(由eclipse打开class文件查看的):

Class字节码指令集代码
  1. 0  new java.lang.String [15]    
  2. 3  dup  
  3. 4  ldc <String "Hello word"> [17]   
  4. 6  invokespecial java.lang.String(java.lang.String) [19]  
  5. 9  astore_1 [s]  
  6. 10  retur  

    下面通俗的解释一下这些指令,详细见《深入Java虚拟机》第二版附表:按操作码助忆符排列的指令集。
    ★ new指令: 在内存的堆区域中为新字符串对象分配足够大的空间,并将对象的实例变量设为默认值。
    ★ ldc指令:在内存的方法区常量池中找到String类型字面值常量表 的入口,然后定位到的"Hello word"所在内存中的位置。
    ★ invokespecial指令:调用指定的类构造器(这里调用的是String(String)这一个构造器。将ldc指令所找到的"Hello word"的内容传入到new指令所开辟在堆中的字符串对象中。
    ★ astore_1:将new指令所开辟堆的内存位置存入局部变量s中

 

    将String s="Hello world";编译成class文件后的指令:

Class字节码指令集代码
  1. 0  ldc <String "Hello world"> [15]  
  2. 2  astore_1 [str]  
  3. 3  return  

    ★ ldc指令:在内存的方法区常量池中找到String类型字面值常量表 的入口,然后定位到的"Hello word"所在内存中的位置(如果常量池中没有"Hello word",则会在其中添加一个"Hello word")。
    ★ astore_1:将ldc指令定位到的常量池中的位置存入局部变量s中

镜头总结: String类型脱光了其实也很普通。真正让她神秘的原因就在于String类型字面值常量表 的存在。

相关问题解决 

   (问题1) 代码1                                                           代码2

              String sa=new String("Hello world");          String sc="Hello world";
              String sb=new String("Hello world");          String sd="Hello world";
              System.out.println(sa==sb);  // false          System.out.println(sc==sd);  // true
              变量sa,sb中存储的内容是JVM在堆中开辟的两个String对象的内存地址。==比较就是sa,sb变量存储的内容,也就是两个不同的内存地址,当然是false;
              变量sc,sd中存储的内容也是地址,但却都是方法区常量池中"Hello word"所在的地址,自然一样。

   (问题2) 代码1                                                          代码2
              String sa = "ab";                                        String sc="ab"+"cd";
              String sb = "cd";                                        String sd="abcd";
              String sab=sa+sb;                                     System.out.println(sc==sd); //true
              String s="abcd";
              System.out.println(sab==s); // false
              代码1中sa+sb被编译以后使用的是StringBuilder.append(String)方法。JVM会在堆中创建一个 StringBuilder类,将sa所指向常量池中的内容"ab"传入,然后调用append(sb所指向的常量池内容)完成字符串合并功能,最后将堆 中StringBuilder对象的地址赋给变量sab。而s存储的是常量池中"abcd"的地址。sab与s地址当然不一样了。
              代码2中"ab"+"cd"会直接在编译阶段就合并成常量"abcd",所以相同的字符串在常量池中的地址也相同了。

【镜头二】  String三姐妹(String,StringBuffer,StringBuilder),谁更性感?
        String不用多说,扒的差不多了。但他还有两个妹妹StringBuffer,StringBuilder长的也不错哦!我们首先对这三姐妹下个定义:
        String(大姐,出生于JDK1.0时代)          不可变字符序列
        StringBuffer(二姐,出生于JDK1.0时代)    线程安全的可变字符序列
        StringBuilder(小妹,出生于JDK1.5时代)   非线程安全的可变字符序列

讨论1、StringBuffer与String的可变性问题。
         我们先看看这两个类的简要源代码:

Java代码
  1. //String   
  2. public final class String  
  3. {  
  4.         private final char value[];  
  5.   
  6.          public String(String original) {  
  7.               // 把原字符串original切分成字符数组并赋给value[];  
  8.          }  
  9. }  
  10.   
  11. //StringBuffer   
  12. public final class StringBuffer extends AbstractStringBuilder  
  13. {  
  14.          char value[]; //继承了父类AbstractStringBuilder中的value[]  
  15.          public StringBuffer(String str) {  
  16.                  super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组  
  17.                  append(str); //将str切分成字符序列并加入到value[]中  
  18.         }  
  19. }  

      很显然,String和StringBuffer中的value[]都用于存储字符序列。但是,
      (1) String中的是常量(final)数组,只能被赋值一次。
      比如:new String("abc")使得value[]={'a','b','c'},之后这个String对象中的value[]再也不能改变了。这也正是大家常说的,String是不可变的原因 。   
      注意:这个对初学者来说有个误区,有人说String str1=new String("abc"); str1=new String("cba");不是改变了字符串str1吗?那么你有必要先搞懂对象引用和对象本身的区别。
      (2) StringBuffer中的value[]就是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾。这样也就改变了value[]的内容和大小了。
      比如:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意构造的长度是str.length()+16)。如果再将这个对象append("abc"),那么这个对象中的value[]={'a','b','c','a','b','c',''....}。这也就是为什么大家说 StringBuffer是可变字符串 的涵义了。
      从这一点也可以看出,StringBuffer中的value[]完全可以作为字符串的缓冲区功能。
               String s1=new String("aaa");
               StringBuffer sb1=new StringBuffer(); //一个字符串缓冲区
               sb1.append(s1);//将字符串s1加进缓冲区
    注意,讨论String和StringBuffer可不可变。本质指对象内部的value[]字符数组可不可变,而不是对象引用可不可变。
讨论2、StringBuffer与StringBuilder的线程安全性问题
     StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized 修饰了,而StringBuilder没有。
     有多线程编程经验的程序员应该知道synchronized。这个关键字是为线程同步机制 设定的。我简要阐述一下synchronized的含义:
      每一个类对象都对应一把锁,当某个线程A调用类对象O中的synchronized方法M时,必须获得对象O的锁才能够执行M方法,否则线程A阻塞。一旦 线程A开始执行M方法,将独占对象O的锁。使得其它需要调用O对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用M方 法。这就是解决线程同步问题的锁机制。
     了解了synchronized的含义以后,大家可能都会有这个感觉。多线程编程中StringBuffered比StringBuilder要安全多了 ,事实确实如此。如果有多个线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer应该是不二选择。
     注意:是不是String也不安全呢?事实上不存在这个问题,String是字符串常量,不可变。线程对于String对象只能读取,无法修改。试问:还有什么不安全的呢?
讨论3、String和StringBuffer的效率问题(这可是个热门话题呀!)

      首先说明一点:StringBuffer和StringBuilder可谓双胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。在Core Java editior 7 中文版P611页中有这么一句话: StringBuffer比StringBuilder的效率稍低
      我们用下面的代码运行1W次字符串的连接操作,测试String,StringBuffer所运行的时间。

Java代码
  1. //测试代码  
  2. public class RunTime{  
  3.     public static void main(String[] args){  
  4.            //位置1  
  5.           long beginTime=System.currentTimeMillis();  
  6.           for(int i=0;i<10000;i++){  
  7.                 //位置2  
  8.           }  
  9.           long endTime=System.currentTimeMillis();  
  10.           System.out.println(endTime-beginTime);     
  11.     }  
  12. }  

    (1) String常量与String变量的"+"操作比较
         首先声明两个概念:String str="Heart"中"Heart"是String常量(存放在常量池中);而str则是String变量,是String对象的引用。
         ▲测试①代码:     (测试代码位置1)  String str="";
                                   (测试代码位置2)  str="Heart"+"Raid";
            [耗时:  0ms]
            
         ▲测试②代码        (测试代码位置1)  String s1="Heart";
                                                           String s2="Raid";
                                                           String str="";
                                  (测试代码位置2)  str=s1+s2;
            [耗时:  15—16ms]
        结论:String常量的“+连接” 稍优于  String变量的“+连接”。
        原因:测试①的"Heart"+"Raid"在编译阶段就已经连接起来,并形成了一个新的字符串常量"HeartRaid"。运行阶段只需要 将"HeartRaid"存入常量池。循环1W次也只不过将常量池中"HeartRaid"的地址取出1W次赋值给String对象的引用而已。
                测试②在编译阶段,编译器并不知道s1和s2代表什么,它会将这句话编译成几个指令,大体上是这样的。
                      首先:StringBuilder temp=new StringBuilder(s1),
                      然后:temp.append(s2)
                      最后:str=temp.toString();
                我们发现,虽然在中间的时候也用到了append()方法,但是在开始和结束的时候分别创建了StringBuilder和String对象。可想而知:调用1W次,是不是就创建了1W次这两中对象呢?不划算。

         不言而喻:String变量的"+"连接操作比String常量操作使用的更加广泛。
     (2)String对象的"累+"连接操作与StringBuffer对象的append()累和连接操作比较。
          ▲测试①代码:     (代码位置1)  String s1="Heart";
                                                       String s="";
                                    (代码位置2)  s=s+s1;
             [耗时:  4200—4500ms]
            
          ▲测试②代码        (代码位置1)  String s1="Heart";
                                                       StringBuffer sb=new StringBuffer();
                                    (代码位置2) sb.append(s1);
             [耗时:  0ms(当循环100000次的时候,耗时大概16—31ms)]
         结论:StringBuffer的append()累和连接    远好于    String对象的"累+"连接
         原因:测试①中s=s+s1我们知道编译器也会使用StringBuilder的append方法,但是首先编译器会将new StringBuilder存放s,然后在通过 StringBuilder.append(s1),最后在new一个新的String对象存放 StringBuilder的连接后的内容 。试想这个过程一次就需要创建一个新的String并赋值给引用s,10000次就需要创建了1W次新String对象。正式因为String中的value[]数组不可变,只能不停的创建新的String来存放变化中的字符串。效率可想而知了。
                  测试②中sb.append(s1);只需要将自己的value[]数组不停的扩大来存放s1即可。无需在堆中创建一大堆的对象。效率高就不足为奇了。
      
         不言而喻:大规模累加字符串的时候,StringBuffer就比她姐姐不知道强多少倍了。
镜头总结: (1) 在编译阶段就能够确定的字符串常量,完全没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高。
                (2)否则,StringBuffer对象的append效率要高于String对象的"+"连接操作。
                (3) intern()方法很有用。
                     当调用String 对象的intern方法时,如果池已经包含一个等于此对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
                     也就是如果我们需要使用new关键字来创建很多内容重复的字符串的话,使用String.intern()方法可以提高效率。

2009年12月15日星期二

[转]linux 32位向64位的移植常见问题

1.      数据截断:

由于long类型变量的运算(赋值、比较、移位等)产生。long定义在x86上为32bits,而在ia64上为64bits.容易在与int型变量运算时出现异常。

处理方法:尽量避免不同类型变量间的运算,避免将长度较长的变量赋值到较短的变量中,统一变量长度可以解决这个问题。简单的对于32位转移到64位可以将所有long定义转换为int定义。

2.      指针存储:

x86平台下,习惯使用int来存储指针,并将指针直接参与到int型的运算中,而64位平台下指针大小为64bits,无法存储到int中。对指针的运算也可能会因为数据长度的不一致导致异常。

处理方法:修改用于存储传递指针的变量为intptr_t 类型定义,以保证平台兼容性

3.      不规范的结构:

未命名或为指定数量的成员可能会出现异常

处理方法:命名未命名的成员,声明类型符号,将long型定义转为int型。

4.      代码中的直接数

直接使用16进制数字进行赋值时(一般会隐含假设该变量为32位变量的前提) 可能出现异常。使用数字定义数据大小,进行移位运算时会出现异常。

处理方法:检查代码中的直接数字是否有表示32位有关的各种形式,如4,32, 0x7fffffff等,替换为宏定义。

 

编写代码时注意可移植化:

1.    32位与64位下使用同样的头文件

2.      使用严禁的格式定义,如:用off_t定义偏移量,用fpos_t定义文件位置, intptr_t定义指针

3.      使用<intypes.h>中定义的整数类型,不使用int,long,long long的传统定义方式。使用带有整形标示符参数的printf函数,不使用%d,%ld的格式化方式。

4.      使用固定宽度或宏定义的整数类型,数字,掩码

5.      对整形变量作边界检查。

6.      32位与64位进程共享内存操作时,使用64位作为操作字长。

[转]typeof、instanceof、constructor与对象类型

转自:http://www.cnblogs.com/aming/archive/2008/10/27/1320277.html

1、typeof 运算符把类型信息以字符串形式返回。typeof 返回六种可能的值:“数字类型”、“字符串类型”、“布尔型”、“对象类型”、“函数类型”和“未定义类型”。
  但对所有的对象和数组类型返回的都是"object",所以它只在区别对象和原始类型的时候才有用。要区一种对象类型和另一种对象类型,必须使用其他的方法。如:instanceof运算符和constructor属性。


2、instanceof运算符。如果 objectclass 或构造函数的实例,则 instanceof 运算符返回 true。如果 object 不是指定类或函数的实例,或者 objectnull,则返回 false


3、constructor属 性。对象的constructor属性引用了此其构造函数,常用于判断求知对象的类型。如给定一个求知的值通过typeof运算符来判断它是原始的值还是 对象。如果是对象,就可以使用constructor属性来判断其类型。但在客户端javascript对象中Object.toString()方法的 默认实现提供了另一种判断求知对象类型的方法。

2009年12月12日星期六

[精品][原创]Java ClassLoader总结

Classloader是Java语言用于载入.class文件。.class文件是包含了java字节码。
Classloader通过loadClass方法载入class文件,得到Class类。再用newInstance();方法获得类的实例。

委托模式:
Java1.2之后,Classloader添加了委托模式,当载入一个class文件时,
  • 首先查找是否已经载入了此类;
  • 如果没有则会先调用它的getParent()方法得父类的Classloader,然后让父类载入class文件;
  • 如果父类载入失败的话那么会自己本身载入class,如果再载入失败则抛出ClassNotFoundException。
这是为了安全考虑,如果我们建了一个包名都一样的String类,那么不使用委托模式会导致不安全。
另外委托模式对自定义的Classloader不起作用。除非自己实现。

使用ClassLoader方法:
  • 方法一:
MyClassLoader cl = new MyClassLoader();
cl.loadClass("test.classloader.A");方法直接使用MyClassLoader载入类。
  • 方法二:
Class.forName("test.classloader.A",true,new MyClassLoader());方法使用MyClassLoader载入类。
  • 方法三:
Class.forName("test.classloader.A");方法使用CallerClassLoader载入类。
new test.classloader.A();方法使用CallerClassLoader载入类。

Caller Classloader指的是当前所在的类装载时使用的Classloader,它可能是System Classloader,也可能是一个自定义的Classloader,这里,我们都称之为Caller Classloader。我们可以通过getClass().getClassLoader()来得到Caller Classloader。例如,存在A类,是被AClassLoader所加载,A.class.getClassLoader()为AClassLoader的实例,它就是A.class的Caller Classloader。

如果在A类中使用new关键字,或者Class.forName(String className)和Class.getResource(String resourceName)方法,那么这时也是使用Caller Classloader来装载类和资源。比如在A类中初始化B类:
/**
* A.java
*/
public void foo() {
B b = new B();
b.setName("b");
}

那么,B类由当前Classloader,也就是AClassloader装载。同样的,修改上述的foo方法,其实现改为:

Class clazz = Class.forName("foo.B");

最终获取到的clazz,也是由AClassLoader所装载。

  • 方法四:
使用Thread的ClassLoader来载入类。
Thread.currentThread().setContextClassLoader(new MyClassLoader());
Thread.currentThread().getContextClassLoader().loadClass("test.classloader.A");

系统默认的三种ClassLoader

(Bootstrap) 装载器:该装载器没有父装载器,它是 JVM 实现的一部分,从 sun.boot.class.path 装载运行时库的核心代码。

扩展 (Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯 Java 代码实现的,它从 java.ext.dirs ( 扩展目录 ) 中装载代码。

系统 (System or Application) 装载器:装载器为扩展装载器,我们都知道在安装 JDK 的时候要设置环境变量 (CLASSPATH ) ,这个类装载器就是从 java.class.path(CLASSPATH 环境变量 ) 中装载代码的,它也是用纯 Java 代码实现的,同时还是用户自定义类装载器的缺省父装载器。


ClassLoader中重要方法:
  1. loadCass方法:loadClass(String name ,boolean resolve) 其中 name 参数指定了 JVM 需要的类的名称 , 该名称以类的全限定名表示,如 Java.lang.Object ; resolve 参数告诉方法是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个方法是 ClassLoader 的入口点。
  2. defineClass方法:这个方法接受类文件的字节数组并把它转换成 Class 对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。
  3. findSystemClass方法:findSystemClass 方法从本地文件系统装入 Java 字节码。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将字节数组转换成 Class 对象。当运行 Java 应用程序时 , 这是 JVM 正常装入类的缺省机制。
  4. resolveClass方法resolveClass(Class c):方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用 loadClass 方法时 , 通过它的 resolve 参数决定是否要进行解析。
  5. findLoadedClass方法:当调用 loadClass 方法装入类时 , 调用 findLoadedClass 方法来查看 ClassLoader 是否已装入这个类 , 如果已装入 , 那么返回 Class 对象 , 否则返回 NULL 。如果强行装载已存在的类 , 将会抛出链接错误。

2009年12月7日星期一

[转]使用负边距创建自适应宽度的流体布局

文章来源:http://www.osmn00.com/?p=24

合理使用负边距技术,可以帮助我们创建很多有意思的布局,比如自适应浏览器宽度的流体布局。国外关于使用负边距创建这类布局的技术文档,我看到的最早是04年 Ryan Brill 发表在 A List Apart 上的 《Creating Liquid Layouts with Negative Margins》 (04年 - -!国内刚小部分人开始关注WEB标准化),本文亦可看做是对其的中文讲解版。

随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。目前国内门户很多都进行了改版,采用目前的主流———960px左右的宽度。

我认为,对于不太复杂的网站,采用百分比进行架构是个不错的主意。自适应布局的应用面还是蛮多的,比如论坛页面、博客页面等。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。

这里需要的用到的技术关键点就一则:负边距。

【简单的布局】

OK。我们现在开始。假设现在需要给自己的博客重新制作前台,界面的设计已经完成,就差代码架构。我们希望博客的界面可以做到:左侧的主要部分是博客文章 内容,这部分需要在不同分辨率浏览器下自适应宽度;而右侧是侧边栏,这部分我们想做成一个固定250px宽,预期效果图如下:



这是个典型的两栏布局,我们来做一个初步的架构
引用:
<div id=”header”>顶部区域</div>
<div id=”mainer”>
<h1>使用负边距创建自适应宽度的流体布局</h1>
<p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
</div>
<div id=”sideBar”>
<h2>最新文章</h2>
<ul>
<li>最新文章一</li>
<li>最新文章二</li>
<li>最新文章三</li>
</ul>
</div>
<div id=”footer”>底部区域</div>
查看测试页面一,这是在没有样式表的情况下页面的显示效果。下面我们给它加上基本的样式表
引用:
body,p,h1,h2,ul {
margin:0;padding:0;
}
#header {
background-color: #A8A754;
}
#footer {
background-color: #A8A754;
clear: both;
}
#mainer {
width: 100%;
margin-right: -250px;
float: left;
}
#sideBar {
float: right;
width: 250px;
}
定义mainer右边距为-250px,使得右边得以空出侧边栏的宽度,放置侧边栏。加上样式表页面请查看测试页面二。OK,现在我们看到左侧的内容区域已经为侧边栏空出了相应的空间,使得侧边栏放置在了它应该出现的位置。

【去除重叠部分】

我们这时会发现,左侧的文字内容部分却和侧边栏有重叠。这时候我们需要另外的一个DIV层,来将左侧文字部分设置一个足够大的右边距,使其不与侧边栏重叠。并将左侧内容部分与侧边栏部分设置不同背景色,以示区分。
引用:
<div id=”mainer”>
<div id=”main”>
<h1>使用负边距创建自适应宽度的流体布局</h1>
<p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
</div>
</div>
增加的CSS
引用:
#sideBar {
color: #FFF;
background-color: #36361A;
}
#main {
margin-right: 250px;
background-color: #616030;
}
查看测试页面三

【自适应高度】

这时候我们又发现了一个问题:如果左侧长度继续加长,那么,右侧侧边栏的下部会出现空白。我们希望可以实现视觉上的左右两栏自适应等高。

这里我们可以给外层的mainer DIV设置一个右对齐纵向重复的背静图片,图片的宽度我们设置为250px,即同侧边栏宽度相同。



CSS部分我们加上
引用:
#mainer {
background: url(bj1.jpg) repeat-y right bottom;
}
多数时候我们并不想设置全部宽度为浏览器的宽度,多数时候我们会设置小于屏幕宽度的百分比。这种情况下,我们可以在左侧内容及右侧侧边栏外再套一个DIV层,并在侧边栏下加上一个清除浮动的层,来达到我们的目的。这个时候我们的XHTML会是下面这个样子。
引用:
    <div id=”header”>顶部区域</div>

    <div id=”wrapper” class=”mid”>
    <div id=”mainer”>
    <div id=”main”>
    <h1>使用负边距创建自适应宽度的流体布局</h1>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    </div>
    </div>

    <div id=”sideBar”>
    <h2>最新文章</h2>
    <ul>
    <li>最新文章一</li>
    <li>最新文章二</li>
    <li>最新文章三</li>
    </ul>
    </div>
    <div class=”clear”></div>
    </div>

    <div id=”footer”>底部区域</div>
相应的CSS
引用:
#wrapper {width: 92%;}
.clearing {clear: both;}
.mid {margin:0 auto;}
查看测试页面四

Ryan Brill 在他的文章里说,外层的wrapper与里面的mainer两个DIV都应该设置背景 background,以便解决IE里的一个BUG。但我这里只设置了mainer DIV的背景,在IE6、IE7、FF里并未发现错误。可能他指的是IE5.x,这里忽略。

【进阶,三栏布局】

掌握了以上的方法,我们会发现制作一个三栏的布局也是很简单的!OK。下面我们把上面的例子变下,我们需要一个三栏的布局,2侧为固定宽度,中部为自适应宽度。这仅需要增加一点DIV。
引用:
    <div id=”header” class=”mid”>顶部区域</div>

    <div id=”wrapper” class=”mid”>
    <div id=”mainer”>
    <div id=”main”>

    <div id=”leftBar”>
    <h2>栏目标题</h2>
    <ul>
    <li>文章标题</li>
    <li>文章标题</li>
    <li>文章标题</li>
    </ul>
    </div>

    <div id=”inmain”>
    <h1>使用负边距创建自适应宽度的流体布局</h1>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    <p>随着越来越大的浏览器的出现及普及,网站界面如何能满足不同分辨率浏览器使用者的浏览需求,逐渐成为前端开发工程师必须面对的问题。采 用百分比进行架构是个不错的主意。以往我们进行这类架构都是使用table表格。但,其实使用很小的技术就可以创建出符合WEB标准化的自适应布局。 </p>
    </div>

    </div>
    </div>

    <div id=”sideBar”>
    <h2>最新文章</h2>
    <ul>
    <li>最新文章一</li>
    <li>最新文章二</li>
    <li>最新文章三</li>
    </ul>
    </div>
    <div class=”clear”> </div>
    </div>

    <div id=”footer” class=”mid”>底部区域</div>
以及另外一个背景图片



CSS部分增加:
引用:
#main {
margin-right: 250px;
background: url(bj2.jpg) #616030 repeat-y left bottom;
}
#leftBar {
float: left;
width: 150px;
}
#inmain {
margin-left: 150px;
}
现在来看看我们的页面,测试页面五