qileilove

blog已经转移至github,大家请访问 http://qaseven.github.io/

Java中线程安全问题个人理解

 线程安全问题是一个比较高深的问题,是很多程序员比较难掌握的一个技术难点,如果一个程序员对线程掌握的很好的话,那么这个程序员的内功修炼的是相当的好。

  在这里我主要说一下我对java中如何保证线程安全的一些个人见解,希望对各位有所帮助,那里有不对的地方敬请给位不吝赐教。

  线程安全问题主要出现在访问临界资源的时候,就是访问同一个对象的时候,可能会出现无法挽回的损失,特别是在关于资金安全方面的时候,当然还有数据库事务方面的问题。他们很类似,都是要保证数据的原子性。

  那么在java中如何保证线程安全呢?

  对与共同使用的对象进行加锁,意思是我使用的时候,那么你就必须等待,等我用完之后你再用,反之依然。就像上厕所,你去的时候我是不能去的。

  如何加锁呢?下面写三个加锁的方式

  首先看一下实例代码

  1. public class TraditionalSynchornizedTest {  
  2.  /** 
  3.   * @param args 
  4.   */ 
  5.  public static void main(String[] args) {  
  6.   new TraditonalSynchornizedTest().sartThread();  
  7.  }  
  8.  public void sartThread(){  
  9.   final Outerput outerput = new Outerput();  
  10.   new Thread(new Runnable(){  
  11.    @Override 
  12.    public void run() {  
  13.     while(true){  
  14.      try {  
  15.       Thread.sleep(5);  
  16.      } catch (InterruptedException e) {  
  17.       e.printStackTrace();  
  18.      }  
  19.      outerput.print("zhangsanfeng");  
  20.     }  
  21.    }  
  22.      
  23.   }).start();  
  24.   new Thread(new Runnable(){  
  25.    @Override 
  26.    public void run() {  
  27.     while(true){  
  28.      try {  
  29.       Thread.sleep(5);  
  30.      } catch (InterruptedException e) {  
  31.       e.printStackTrace();  
  32.      }  
  33.      outerput.print("luxiaofeng");  
  34.     }  
  35.    }  
  36.      
  37.   }).start();  
  38.  }  
  39.  public class Outerput{  
  40.   public void print(String name){  
  41.    for(int i = 0;i < name.length(); i++){  
  42.     System.out.print(name.charAt(i));  
  43.    }  
  44.    System.out.println();  
  45.   }  
  46.  }  
  47. }

  以上代码没有对共同持有的对象outerput加锁,所以会出现线程安全问题

1、对代码块加锁

  对共同持有的对象加锁可以把内部类写成这样的

  1. public class Outerput{  
  2.   public void print(String name){  
  3.    synchronized (this) {  
  4.     for(int i = 0;i < name.length(); i++){  
  5.      System.out.print(name.charAt(i));  
  6.     }  
  7.     System.out.println();  
  8.    }  
  9.   }  
  10.  }

  2、对非静态方法加锁,加锁的对象是this

  1. public class Outerput{  
  2.   public synchronized void print(String name){  
  3.    for(int i = 0;i < name.length(); i++){  
  4.     System.out.print(name.charAt(i));  
  5.    }  
  6.    System.out.println();  
  7.   }  
  8.  }

  3、对静态方法加锁的对象到底是谁?

  1. public static synchronized  void print2(String name){  
  2.    for(int i = 0;i < name.length(); i++){  
  3.     System.out.print(name.charAt(i));  
  4.    }  
  5.    System.out.println();  
  6.   }

  其实加锁的对象是字节码对象,Outerput.class

  如果和非静态方法同时持有同一个对象时,可以持有同一个字节码对象。



posted @ 2012-02-23 17:09 顺其自然EVO 阅读(151) | 评论 (0)编辑 收藏

利用Java进行MySql数据库的导入和导出

利用Java进行MySql数据库的导入和导出

 利用Java来进行Mysql数据库的导入和导出的总体思想是通过Java来调用命令窗口执行相应的命令。

  MySql导出数据库的命令如下:

mysqldump -uusername -ppassword -hhost -Pport exportDatabaseName > exportPath

  利用Java调用命令窗口执行命令来进行MySql导入数据库一般分三步走:

  第一步:登录Mysql数据库,在登录数据库的时候也可以指定登录到哪个数据库,如果指定了则可以跳过第二步;

  第二步:切换数据库到需要导入的目标数据库

  第三步:利用命令开始导入

  在进行导出的时候,需要注意命令语句的运行环境,如果已经将mysql安装路径下的bin加入到系统的path变量中,那么在导出的时候可以直接使用命令语句,否则,就需要在执行命令语句的时候加上命令所在位置的路径,即mysql安装路径想的bin下的mysqldump命令。

  基本代码如下:

  1. import java.io.IOException;  
  2. import java.io.InputStream;  
  3. import java.io.OutputStream;  
  4. import java.io.OutputStreamWriter;  
  5. import java.util.Properties;  
  6. /** 
  7.  * 在进行导出的时候,需要注意命令语句的运行环境,如果已经将mysql安装路径下的bin加入到 
  8.  * 系统的path变量中,那么在导出的时候可以直接使用命令语句,否则,就需要在执行命令语句的 
  9.  * 时候加上命令所在位置的路径,即mysql安装路径想的bin下的mysqldump命令 
  10.  * @author andy 
  11.  * 
  12.  */ 
  13. public class MySqlImportAndExport {  
  14.     public static void main(String args[]) throws IOException {  
  15.         InputStream is = MySqlImportAndExport.class.getClassLoader().getResourceAsStream("jdbc.properties");  
  16.         Properties properties = new Properties();  
  17.         properties.load(is);  
  18. //      MySqlImportAndExport.export(properties);//这里简单点异常我就直接往上抛 
  19.         MySqlImportAndExport.importSql(properties);  
  20.     }  
  21.       
  22.     /** 
  23.      * 根据属性文件的配置导出指定位置的指定数据库到指定位置 
  24.      * @param properties 
  25.      * @throws IOException 
  26.      */ 
  27.     public static void export(Properties properties) throws IOException {  
  28.         Runtime runtime = Runtime.getRuntime();  
  29.         String command = getExportCommand(properties);  
  30.         runtime.exec(command);//这里简单一点异常我就直接往上抛 
  31.     }  
  32.       
  33.     /** 
  34.      * 根据属性文件的配置把指定位置的指定文件内容导入到指定的数据库中 
  35.      * 在命令窗口进行mysql的数据库导入一般分三步走: 
  36.      * 第一步是登到到mysql; mysql -uusername -ppassword -hhost -Pport -DdatabaseName;如果在登录的时候指定了数据库名则会 
  37.      * 直接转向该数据库,这样就可以跳过第二步,直接第三步;  
  38.      * 第二步是切换到导入的目标数据库;use importDatabaseName; 
  39.      * 第三步是开始从目标文件导入数据到目标数据库;source importPath; 
  40.      * @param properties 
  41.      * @throws IOException  
  42.      */ 
  43.     public static void importSql(Properties properties) throws IOException {  
  44.         Runtime runtime = Runtime.getRuntime();  
  45.         //因为在命令窗口进行mysql数据库的导入一般分三步走,所以所执行的命令将以字符串数组的形式出现 
  46.         String cmdarray[] = getImportCommand(properties);//根据属性文件的配置获取数据库导入所需的命令,组成一个数组 
  47.         //runtime.exec(cmdarray);//这里也是简单的直接抛出异常 
  48.         Process process = runtime.exec(cmdarray[0]);  
  49.         //执行了第一条命令以后已经登录到mysql了,所以之后就是利用mysql的命令窗口 
  50.         //进程执行后面的代码 
  51.         OutputStream os = process.getOutputStream();  
  52.         OutputStreamWriter writer = new OutputStreamWriter(os);  
  53.         //命令1和命令2要放在一起执行 
  54.         writer.write(cmdarray[1] + "\r\n" + cmdarray[2]);  
  55.         writer.flush();  
  56.         writer.close();  
  57.         os.close();  
  58.     }  
  59.       
  60.     /** 
  61.      * 利用属性文件提供的配置来拼装命令语句 
  62.      * 在拼装命令语句的时候有一点是需要注意的:一般我们在命令窗口直接使用命令来 
  63.      * 进行导出的时候可以简单使用“>”来表示导出到什么地方,即mysqldump -uusername -ppassword databaseName > exportPath, 
  64.      * 但在Java中这样写是不行的,它需要你用-r明确的指出导出到什么地方,如: 
  65.      * mysqldump -uusername -ppassword databaseName -r exportPath。 
  66.      * @param properties 
  67.      * @return 
  68.      */ 
  69.     private static String getExportCommand(Properties properties) {  
  70.         StringBuffer command = new StringBuffer();  
  71.         String username = properties.getProperty("jdbc.username");//用户名 
  72.         String password = properties.getProperty("jdbc.password");//用户密码 
  73.         String exportDatabaseName = properties.getProperty("jdbc.exportDatabaseName");//需要导出的数据库名 
  74.         String host = properties.getProperty("jdbc.host");//从哪个主机导出数据库,如果没有指定这个值,则默认取localhost 
  75.         String port = properties.getProperty("jdbc.port");//使用的端口号 
  76.         String exportPath = properties.getProperty("jdbc.exportPath");//导出路径 
  77.           
  78.         //注意哪些地方要空格,哪些不要空格 
  79.         command.append("mysqldump -u").append(username).append(" -p").append(password)//密码是用的小p,而端口是用的大P。 
  80.         .append(" -h").append(host).append(" -P").append(port).append(" ").append(exportDatabaseName).append(" -r ").append(exportPath);  
  81.         return command.toString();  
  82.     }  
  83.       
  84.     /** 
  85.      * 根据属性文件的配置,分三步走获取从目标文件导入数据到目标数据库所需的命令 
  86.      * 如果在登录的时候指定了数据库名则会 
  87.      * 直接转向该数据库,这样就可以跳过第二步,直接第三步;  
  88.      * @param properties 
  89.      * @return 
  90.      */ 
  91.     private static String[] getImportCommand(Properties properties) {  
  92.         String username = properties.getProperty("jdbc.username");//用户名 
  93.         String password = properties.getProperty("jdbc.password");//密码 
  94.         String host = properties.getProperty("jdbc.host");//导入的目标数据库所在的主机 
  95.         String port = properties.getProperty("jdbc.port");//使用的端口号 
  96.         String importDatabaseName = properties.getProperty("jdbc.importDatabaseName");//导入的目标数据库的名称 
  97.         String importPath = properties.getProperty("jdbc.importPath");//导入的目标文件所在的位置 
  98.         //第一步,获取登录命令语句 
  99.         String loginCommand = new StringBuffer().append("mysql -u").append(username).append(" -p").append(password).append(" -h").append(host)  
  100.         .append(" -P").append(port).toString();  
  101.         //第二步,获取切换数据库到目标数据库的命令语句 
  102.         String switchCommand = new StringBuffer("use ").append(importDatabaseName).toString();  
  103.         //第三步,获取导入的命令语句 
  104.         String importCommand = new StringBuffer("source ").append(importPath).toString();  
  105.         //需要返回的命令语句数组 
  106.         String[] commands = new String[] {loginCommand, switchCommand, importCommand};  
  107.         return commands;  
  108.     }  
  109.       
  110. }

  上述使用的jdbc.properties文件

  1. jdbc.username=root  
  2. jdbc.password=password 
  3. jdbc.host=localhost  
  4. jdbc.port=3306  
  5. jdbc.exportDatabaseName=dbName  
  6. jdbc.exportPath=d\:\\dbName.sql  
  7. jdbc.importDatabaseName=test  
  8. jdbc.importPath=d\:\\dbName.sql

posted @ 2012-02-22 14:58 顺其自然EVO 阅读(276) | 评论 (0)编辑 收藏

Java自定义范型的应用技巧

 我们在JAVA中处处都用到了范型,JAVA中的范型是从C++模板继承来的,不过JAVA的范型的功能远远没有C++那么强大。

  我们知道在C++中模板可以很方便的代替任意类型的数据如下;

template<class T>
void show(T x)
{
cout<<x<<endl ;
}

  上面的T可以代表任意类型的数据,这样不是大大减少了函数的重载次数,提高了效率呢。java是从C++过来的,理解了C++,jav也不在话下。

  在java中自定义范型也可以用在方法上如下:

  1、//这样声明的范型可以代替任意类型数据我们市场用到的键值对Map.Entry<K,V>不就是给予范型的吗

  KV都可以代替任意类型的值,但是在java中范型的实际类型必须是引用类型

<K,V> void get(K k,V v)
{

}

  2、Java中的范型不能像C++那么灵活

<T>  T  add(T a,T b)
{
   //return  a+b   ;//很多人以为java也想C++一样可以这样 ,但是不可以 。     
return  null;
}

  这个返回的null也是有类型限制的,比如上面的ab分别是Integer和String那么就会取他们共同的基类Object做为返回值类型,其他的同理

  3、实现任意类型的数组的成员值的交换,注意在自定义范型中范型的实际类型只能是引用数据类型不能是基本数据类型

public  static <T> void  swap(T[]a,int x,int y)
{
  T  tem  =a[x]  ;
  a[x]=a[y]  ;
  a[y]=tem ;
 
}

  上面这个方法如果我  swap(new Integer[]{1,2,3,4,5},1,2);       //这样就会自动交换下标12的值

  但是这样调用就错了   swao(new int[]{1,2,3,5,6},2,3) ;  //所以说Java的范型的实际类型 只能是引用数据类型

  4、

  <T  extends  String>     表示类型只能是String或者String的派生类
  <T super  String >   表示范型类型只能是String或者String的父类

  用法同上

  5、

  下面这个函数利用范型来实现类型自动转换的功能

public static  <T> T autoConvert(Object obj)  //因为返回值是 T标识任意类型 所哟可以 将返回结果赋值给任意类型对象
 {
  return (T)obj;
 } 

Object  obj=="";

String str=autoConvert(obj);

  可以完成自动转换,因为范型T代表任意类型,因此他可以赋值给String类型的对象

6、将任意类型的对象填充到任意类型的数组中,与是fillArray(newInteger[]{2,3,4},"ddd");这样调用是正确的,这样做忽略类型限制

public  static <T> void  fillArray(T[] a,T b)  //将任意一个对象填充到任意类型的数组
 {
  for(int i =0;i<a.length;i++)
  {
   a[i] =b ;
  }
 }

  7、以自定义范型的形式显示一个集合的数据,下面一个是利用自定义范型一个是利用通配符来实现,但是不同的是利用通配符操作的集合不能向集合中插入元素

  但是自定义范型却可以。原因是通配符代表的集合我们不知道集合内部具体元素是什么类型所以不能对集合进行add操作。

public static  <T> void showCollection(Collection<T> col,T  obj)  //利用范型来输出任意类型集合
 {  
  col.add(obj) ;
  for(T a:col)
  {
   System.out.println(a);
  }
 }

public static void showCollection(Collection<?> col)  //利用范型来输出任意类型集合 
{  
  for(Object obj:col)
  {
   System.out.println(obj);
  }
}

  8、如果一个类中多个方法都需要范型那么就是用类级别的范型。例如

class  A<E>  
{  
     public void  add(E obj){}
     public  E  get(){}  
     private E data;  
}

  这样声明范型和在函数前面声明其实是一样的只不过是在类的级别上作用于整个类而已

  9、要注意范型只是给编译器看的。

  也就是说Vector<Integer>Vector<String>他们用到的都是同一份字节码,字节码只有class文件加载到内存中的时候才有

  所以在一个类中下面2个方法不能同时存在

void show(Vector<Integer>) {}
void show(Vector<String>){}

  这两个方法都不是重载因为编译后要去掉类型信息。

posted @ 2012-02-21 16:58 顺其自然EVO 阅读(220) | 评论 (0)编辑 收藏

Java类加载器以及类加载器的委托模型

 我们知道,我们在Java中用到的所有的类都是通过类加载器ClassLoader加载到JVM中的,我们还知道类加载器也对应着一个类,既然这样那么我们会想那么ClassLoader类是由谁加载的呢?

  其实在Java中有许许多多的类加载器,我们甚至可以写自己的类加载器。

  其中主要三个类加载器(他们是树形关系)是:

  BootStrap:在java虚拟机启动的时候会利用这个类加载器来加载 JDK安装目录下的 /JRE/LIB/rt.jar 也就是系统默认导入的一些类例如System类,这个类加载器不是类 。只是作为一个java中类的起源工具。

  ExpClassLoader:这个类加载器加载JDK安装目录下的/JRE/LIB/ext 目录中的类 我们只要把我们的类打包成JAR包放在这里即可。

  AppClassLoader:我们在java程序中classpath对应的类都有这个AppClassLoader导入进来。

  看下面一段代码:

  1. package me.test;  
  2. /** 
  3. *   BootStrap   
  4. *   加载  JRE/lib/rt.jar 包中的类   包括我们常用到的类 
  5. *   
  6. *   ExtClassLoader 
  7. *   专门家在 JDK/JRE/libEXT/*.jar  中的类  只要把我们的类放在这里 就会被 这个加载器加载  
  8. * 
  9. *   AppClassLoader   
  10. *   加载ClassPath指定的所有jar和目录 
  11. * 
  12. * **/ 
  13. public class Test1  
  14. {    
  15. public static void main(String []args)  
  16. {  
  17.      
  18.   System.out.println(Test1.class.getClassLoader().getClass().getName() );    //获取主类的类加载器 
  19.   System.out.println(System.class.getClassLoader()); 
  20. //BootStrap    获取System类的类加载器  因为加载器是 BootStrap所以返回null  以为内他不是一个类 
  21.   ClassLoader l=Test1.class.getClassLoader() ;  //获取Test1的类加载器 
  22.   while(l!=null)   //循环出 ClassLoader树 
  23.   {  
  24.    System.out.println(l.getClass().getName());    
  25.    l=l.getParent();  
  26.   }  
  27.    
  28.   System.out.println(l);  
  29.    
  30. }  
  31. }

  ClassLoader的委托模型

  比如说我们在加载一个类的时候AppClassLoader他先让BootStrap来加载类,如果BootStrap已经加载了,那么就返回。如果找不到这个类那么BootStrap就传递给ExtClassLoader 来查找,和BootStrap一样。如果找到就加载,如果找不到就继续传递给AppClassLoader 来加载。如果AppClassLoader还找不到的话,那么AppClassLoader就会跑出ClassNotFoundException 异常。

  我们为什么不利用AppClassLoader下级的加载器呢?因为AppClassLoader下级可能有多个类加载器多个类加载器相互独立,如果加载类那么就会导致内存中出现多份字节码,造成不必要的的内存浪费。这就是类加载器的委托模型。

posted @ 2012-02-21 16:57 顺其自然EVO 阅读(175) | 评论 (0)编辑 收藏

Java堆内存的10个要点

  当我开始学习Java编程时,我不知道什么是堆内存或堆空间,我甚至不知道当对象创建时,它们被放在了哪里。当我开始正式写一些程序后,我会经常遇到java.lang.outOfMemoryError的报错,之后我才开始关注什么是堆内存或者说堆空间(heap space)。对大多数程序员都经历过这样的过程,因为学习一种语言是非常容易来的,但是学习基础是非常难的,因为没有什么特定的流程让你学习编程的每个基础,使你发觉编程的秘诀。

  对于程序员来说,知道堆空间,设置堆空间,处理堆空间的outOfMemoryError错误,分析heap dump是非常重要的。这个关于Java堆的教程是给我刚开始学编程的兄弟看的。如果你知道这个基础知识或者知道底层发生了什么,当然可能帮助不是那么大。除非你知道了对象被创建在堆中,否则你不会意识到OutOfMemoryError是发生在堆空间中的。我尽可能的将我所知道的所有关于堆的知识都写下来了,也希望你们能够尽可能多的贡献和分享你的知识,以便可以让其他人也受益。

  Java中的堆空间是什么?

  当Java程序开始运行时,JVM会从操作系统获取一些内存。JVM使用这些内存,这些内存的一部分就是堆内存。堆内存通常在存储地址的底层,向上排列。当一个对象通过new关键字或通过其他方式创建后,对象从堆中获得内存。当对象不再使用了,被当做垃圾回收掉后,这些内存又重新回到堆内存中。要学习垃圾回收,请阅读”Java中垃圾回收的工作原理”。

  如何增加Java堆空间

  在大多数32位机、Sun的JVM上,Java的堆空间默认的大小为128MB,但也有例外,例如在32未Solaris操作系统(SPARC平台版本)上,默认的最大堆空间和起始堆空间大小为 -Xms=3670K 和 -Xmx=64M。对于64位操作系统,一般堆空间大小增加约30%。但你使用Java 1.5的throughput垃圾回收器,默认最大的堆大小为物理内存的四分之一,而起始堆大小为物理内存的十六分之一。要想知道默认的堆大小的方法,可以用默认的设置参数打开一个程序,使用JConsole(JDK 1.5之后都支持)来查看,在VM Summary页面可以看到最大的堆大小。

  用这种方法你可以根据你的程序的需要来改变堆内存大小,我强烈建议采用这种方法而不是默认值。如果你的程序很大,有很多对象需要被创建的话,你可以用-Xms and -Xmx这两个参数来改变堆内存的大小。Xms表示起始的堆内存大小,Xmx表示最大的堆内存的大小。另外有一个参数 -Xmn,它表示new generation(后面会提到)的大小。有一件事你需要注意,你不能任意改变堆内存的大小,你只能在启动JVM时设定它。

  堆和垃圾回收

  我们知道对象创建在堆内存中,垃圾回收这样一个进程,它将已死对象清除出堆空间,并将这些内存再还给堆。为了给垃圾回收器使用,堆主要分成三个区域,分别叫作New Generation,Old Generation或叫Tenured Generation,以及Perm space。New Generation是用来存放新建的对象的空间,在对象新建的时候被使用。如果长时间还使用的话,它们会被垃圾回收器移动到Old Generation(或叫Tenured Generation)。Perm space是JVM存放Meta数据的地方,例如类,方法,字符串池和类级别的详细信息。你可以查看“Java中垃圾回收的工作原理”来获得更多关于堆和垃圾回收的信息。

  Java堆中的OutOfMemoryError错误

  当JVM启动时,使用了-Xms 参数设置的对内存。当程序继续进行,创建更多对象,JVM开始扩大堆内存以容纳更多对象。JVM也会使用垃圾回收器来回收内存。当快达到-Xmx设置的最大堆内存时,如果没有更多的内存可被分配给新对象的话,JVM就会抛出java.lang.outofmemoryerror,你的程序就会当掉。在抛出 OutOfMemoryError之前,JVM会尝试着用垃圾回收器来释放足够的空间,但是发现仍旧没有足够的空间时,就会抛出这个错误。为了解决这个问题,你需要清楚你的程序对象的信息,例如,你创建了哪些对象,哪些对象占用了多少空间等等。你可以使用profiler或者堆分析器来处理 OutOfMemoryError错误。”java.lang.OutOfMemoryError: Java heap space”表示堆没有足够的空间了,不能继续扩大了。”java.lang.OutOfMemoryError: PermGen space”表示permanent generation已经装满了,你的程序不能再装在类或者再分配一个字符串了。

  Java Heap dump

  Heap dump是在某一时间对Java堆内存的快照。它对于分析堆内存或处理内存泄露和Java.lang.outofmemoryerror错误是非常有用的。在JDK中有一些工具可以帮你获取heap dump,也有一些堆分析工具来帮你分析heap dump。你可以用“jmap”来获取heap dump,它帮你创建heap dump文件,然后,你可以用“jhat”(堆分析工具)来分析这些heap dump。

  Java堆内存(heap memory)的十个要点:

  1、Java堆内存是操作系统分配给JVM的内存的一部分。

  2、当我们创建对象时,它们存储在Java堆内存中。

  3、为了便于垃圾回收,Java堆空间分成三个区域,分别叫作New Generation, Old Generation或叫作Tenured Generation,还有Perm Space。

  4、你可以通过用JVM的命令行选项 -Xms, -Xmx, -Xmn来调整Java堆空间的大小。不要忘了在大小后面加上”M”或者”G”来表示单位。举个例子,你可以用 -Xmx256m来设置堆内存最大的大小为256MB。

  5、你可以用JConsole或者 Runtime.maxMemory(), Runtime.totalMemory(), Runtime.freeMemory()来查看Java中堆内存的大小。

  6、你可以使用命令“jmap”来获得heap dump,用“jhat”来分析heap dump。

  7、Java堆空间不同于栈空间,栈空间是用来储存调用栈和局部变量的。

  8、Java垃圾回收器是用来将死掉的对象(不再使用的对象)所占用的内存回收回来,再释放到Java堆空间中。

  9、当你遇到java.lang.outOfMemoryError时,不要紧张,有时候仅仅增加堆空间就可以了,但如果经常出现的话,就要看看Java程序中是不是存在内存泄露了。

  10、请使用Profiler和Heap dump分析工具来查看Java堆空间,可以查看给每个对象分配了多少内存。

posted @ 2012-02-20 15:16 顺其自然EVO 阅读(155) | 评论 (0)编辑 收藏

编写高质量代码:改善Java程序的151个建议(1)

 第1章 Java开发中通用的方法和准则

  Thereasonablemanadaptshimselftotheworld;theunreasonableonepersistsintryingtoadapttheworldtohimself.

  明白事理的人使自己适应世界;不明事理的人想让世界适应自己。

  —萧伯纳

  Java的世界丰富又多彩,但同时也布满了荆棘陷阱,大家一不小心就可能跌入黑暗深渊,只有在了解了其通行规则后才能使自己在技术的海洋里遨游飞翔,恣意驰骋。

  “千里之行始于足下”,本章主要讲述与Java语言基础有关的问题及建议的解决方案,例如常量和变量的注意事项、如何更安全地序列化、断言到底该如何使用等。

  建议1:不要在常量和变量中出现易混淆的字母

  包名全小写,类名首字母全大写,常量全部大写并用下划线分隔,变量采用驼峰命名法(CamelCase)命名等,这些都是最基本的Java编码规范,是每个Javaer都应熟知的规则,但是在变量的声明中要注意不要引入容易混淆的字母。尝试阅读如下代码,思考一下打印出的i等于多少:

  1. public class Client {  
  2.      public static void main(String[] args) {  
  3.            long i = 1l;  
  4.            System.out.println("i的两倍是:" + (i+i));  
  5.      }  
  6. }

  肯定有人会说:这么简单的例子还能出错?运行结果肯定是22!实践是检验真理的唯一标准,将其拷贝到Eclipse中,然后Run一下看看,或许你会很奇怪,结果是2,而不是22,难道是Eclipse的显示有问题,少了个“2”?

  因为赋给变量i的数字就是“1”,只是后面加了长整型变量的标示字母“l”而已。别说是我挖坑让你跳,如果有类似程序出现在项目中,当你试图通过阅读代码来理解作者的思想时,此情此景就有可能会出现。所以,为了让您的程序更容易理解,字母“l”(还包括大写字母“O”)尽量不要和数字混用,以免使阅读者的理解与程序意图产生偏差。如果字母和数字必须混合使用,字母“l”务必大写,字母“O”则增加注释。

  注意 字母“l”作为长整型标志时务必大写。

  建议2:莫让常量蜕变成变量

  常量蜕变成变量?你胡扯吧,加了final和static的常量怎么可能会变呢?不可能二次赋值的呀。真的不可能吗?看我们神奇的魔术,代码如下:

  1. public class Client {     
  2.      public static void main(String[] args) {  
  3.            System.out.println("常量会变哦:" + Const.RAND_CONST);  
  4.      }  
  5. }  
  6. /*接口常量*/  
  7. interface Const{  
  8.     //这还是常量吗?  
  9.     public static final int RAND_CONST = new Random().nextInt();  
  10. }

  RAND_CONST是常量吗?它的值会变吗?绝对会变!这种常量的定义方式是极不可取的,常量就是常量,在编译期就必须确定其值,不应该在运行期更改,否则程序的可读性会非常差,甚至连作者自己都不能确定在运行期发生了何种神奇的事情。

  甭想着使用常量会变的这个功能来实现序列号算法、随机种子生成,除非这真的是项目中的唯一方案,否则就放弃吧,常量还是当常量使用。

  注意:务必让常量的值在运行期保持不变。

建议3:三元操作符的类型务必一致

  三元操作符是if-else的简化写法,在项目中使用它的地方很多,也非常好用,但是好用又简单的东西并不表示就可以随便用,我们来看看下面这段代码:

  1. public class Client {  
  2.      public static void main(String[] args) {  
  3.            int i = 80;  
  4.            String s = String.valueOf(i<100?90:100);  
  5.            String s1 = String.valueOf(i<100?90:100.0);  
  6.            System.out.println("两者是否相等:"+s.equals(s1));  
  7.      }  
  8. }

  分析一下这段程序:i是80,那它当然小于100,两者的返回值肯定都是90,再转成String类型,其值也绝对相等,毋庸置疑的。恩,分析得有点道理,但是变量s中三元操作符的第二个操作数是100,而s1的第二个操作数是100.0,难道没有影响吗?不可能有影响吧,三元操作符的条件都为真了,只返回第一个值嘛,与第二个值有一毛钱的关系吗?貌似有道理。

  果真如此吗?我们通过结果来验证一下,运行结果是:“两者是否相等:false”,什么?不相等,Why?

  问题就出在了100和100.0这两个数字上,在变量s中,三元操作符中的第一个操作数(90)和第二个操作数(100)都是int类型,类型相同,返回的结果也就是int类型的90,而变量s1的情况就有点不同了,第一个操作数是90(int类型),第二个操作数却是100.0,而这是个浮点数,也就是说两个操作数的类型不一致,可三元操作符必须要返回一个数据,而且类型要确定,不可能条件为真时返回int类型,条件为假时返回float类型,编译器是不允许如此的,所以它就会进行类型转换了,int型转换为浮点数90.0,也就是说三元操作符的返回值是浮点数90.0,那这当然与整型的90不相等了。这里可能有读者疑惑了:为什么是整型转为浮点,而不是浮点转为整型呢?这就涉及三元操作符类型的转换规则:

  若两个操作数不可转换,则不做转换,返回值为Object类型。

  若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。

  若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S类型(可以参考“建议22”,会对该问题进行展开描述)。

  若两个操作数都是直接量数字(Literal),则返回值类型为范围较大者。

  知道是什么原因了,相应的解决办法也就有了:保证三元操作符中的两个操作数类型一致,即可减少可能错误的发生。

  建议4:避免带有变长参数的方法重载

  在项目和系统的开发中,为了提高方法的灵活度和可复用性,我们经常要传递不确定数量的参数到方法中,在Java 5之前常用的设计技巧就是把形参定义成Collection类型或其子类类型,或者是数组类型,这种方法的缺点就是需要对空参数进行判断和筛选,比如实参为null值和长度为0的Collection或数组。而 Java 5引入变长参数(varags)就是为了更好地提高方法的复用性,让方法的调用者可以“随心所欲”地传递实参数量,当然变长参数也是要遵循一定规则的,比如变长参数必须是方法中的最后一个参数;一个方法不能定义多个变长参数等,这些基本规则需要牢记,但是即使记住了这些规则,仍然有可能出现错误,我们来看如下代码:

  1. public class Client {     
  2.      //简单折扣计算  
  3.      public void calPrice(int price,int discount){  
  4.            float knockdownPrice =price * discount / 100.0F;  
  5.            System.out.println("简单折扣后的价格是:"+formateCurrency(knockdownPrice));  
  6.      }    
  7.      //复杂多折扣计算  
  8.      public void calPrice(int price,int... discounts){  
  9.            float knockdownPrice = price;  
  10.            for(int discount:discounts){  
  11.                    knockdownPriceknockdownPrice = knockdownPrice * discount / 100;  
  12.         }  
  13.         System.out.println("复杂折扣后的价格是:" +formateCurrency(knockdownPrice));  
  14.      }  
  15.      //格式化成本的货币形式  
  16.      private String formateCurrency(float price){  
  17.             return NumberFormat.getCurrencyInstance().format(price/100);  
  18.      }  
  19.       
  20.      public static void main(String[] args) {  
  21.            Client client = new Client();  
  22.            //499元的货物,打75折  
  23.            client.calPrice(49900, 75);  
  24.     }  
  25. }

  这是一个计算商品价格折扣的模拟类,带有两个参数的calPrice方法(该方法的业务逻辑是:提供商品的原价和折扣率,即可获得商品的折扣价)是一个简单的折扣计算方法,该方法在实际项目中经常会用到,这是单一的打折方法。而带有变长参数的calPrice方法则是较复杂的折扣计算方式,多种折扣的叠加运算(模拟类是一种比较简单的实现)在实际生活中也是经常见到的,比如在大甩卖期间对VIP会员再度进行打折;或者当天是你的生日,再给你打个9折,也就是俗话说的“折上折”。

  业务逻辑清楚了,我们来仔细看看这两个方法,它们是重载吗?当然是了,重载的定义是“方法名相同,参数类型或数量不同”,很明显这两个方法是重载。但是再仔细瞧瞧,这个重载有点特殊:calPrice(int price,int... discounts)的参数范畴覆盖了calPrice(int price,int discount)的参数范畴。那问题就出来了:对于calPrice(49900,75)这样的计算,到底该调用哪个方法来处理呢?

  我们知道Java编译器是很聪明的,它在编译时会根据方法签名(Method Signature)来确定调用哪个方法,比如calPrice(499900,75,95)这个调用,很明显75和95会被转成一个包含两个元素的数组,并传递到calPrice(int price,in.. discounts)中,因为只有这一个方法签名符合该实参类型,这很容易理解。但是我们现在面对的是calPrice(49900,75)调用,这个“75”既可以被编译成int类型的“75”,也可以被编译成int数组“{75}”,即只包含一个元素的数组。那到底该调用哪一个方法呢?我们先运行一下看看结果,运行结果是:

  简单折扣后的价格是:¥374.25。

  看来是调用了第一个方法,为什么会调用第一个方法,而不是第二个变长参数方法呢?因为Java在编译时,首先会根据实参的数量和类型(这里是2个实参,都为int类型,注意没有转成int数组)来进行处理,也就是查找到calPrice(int price,int discount)方法,而且确认它是否符合方法签名条件。现在的问题是编译器为什么会首先根据2个int类型的实参而不是1个int类型、1个int数组类型的实参来查找方法呢?这是个好问题,也非常好回答:因为int是一个原生数据类型,而数组本身是一个对象,编译器想要“偷懒”,于是它会从最简单的开始“猜想”,只要符合编译条件的即可通过,于是就出现了此问题。

  问题是阐述清楚了,为了让我们的程序能被“人类”看懂,还是慎重考虑变长参数的方法重载吧,否则让人伤脑筋不说,说不定哪天就陷入这类小陷阱里了。

  建议5:别让null值和空值威胁到变长方法

  上一建议讲解了变长参数的重载问题,本建议还会继续讨论变长参数的重载问题。上一建议的例子是变长参数的范围覆盖了非变长参数的范围,这次我们从两个都是变长参数的方法说起,代码如下:

  1. public class Client {  
  2.      public void methodA(String str,Integer... is){       
  3.      }  
  4.       
  5.      public void methodA(String str,String... strs){          
  6.      }  
  7.       
  8.      public static void main(String[] args) {  
  9.            Client client = new Client();  
  10.            client.methodA("China", 0);  
  11.            client.methodA("China", "People");  
  12.            client.methodA("China");  
  13.            client.methodA("China",null);  
  14.      }  
  15. }

  两个methodA都进行了重载,现在的问题是:上面的代码编译通不过,问题出在什么地方?看似很简单哦。

  有两处编译通不过:client.methodA("China")和client.methodA("China",null),估计你已经猜到了,两处的提示是相同的:方法模糊不清,编译器不知道调用哪一个方法,但这两处代码反映的代码味道可是不同的。

  对于methodA("China")方法,根据实参“China”(String类型),两个方法都符合形参格式,编译器不知道该调用哪个方法,于是报错。我们来思考这个问题:Client类是一个复杂的商业逻辑,提供了两个重载方法,从其他模块调用(系统内本地调用或系统外远程调用)时,调用者根据变长参数的规范调用, 传入变长参数的实参数量可以是N个(N>=0),那当然可以写成client.methodA("china")方法啊!完全符合规范,但是这却让编译器和调用者都很郁闷,程序符合规则却不能运行,如此问题,谁之责任呢?是Client类的设计者,他违反了KISS原则(Keep It Simple, Stupid,即懒人原则),按照此规则设计的方法应该很容易调用,可是现在在遵循规范的情况下,程序竟然出错了,这对设计者和开发者而言都是应该严禁出现的。

  对于client.methodA("china",null)方法,直接量null是没有类型的,虽然两个methodA方法都符合调用请求,但不知道调用哪一个,于是报错了。我们来体会一下它的坏味道:除了不符合上面的懒人原则外,这里还有一个非常不好的编码习惯,即调用者隐藏了实参类型,这是非常危险的,不仅仅调用者需要“猜测”该调用哪个方法,而且被调用者也可能产生内部逻辑混乱的情况。对于本例来说应该做如下修改:

  1. public static void main(String[] args) {  
  2.      Client client = new Client();  
  3.      String[] strs = null;  
  4.      client.methodA("China",strs);  
  5. }

  也就是说让编译器知道这个null值是String类型的,编译即可顺利通过,也就减少了错误的发生。

posted @ 2012-02-17 16:46 顺其自然EVO 阅读(388) | 评论 (0)编辑 收藏

Java Socket重要参数讲解

Java Socket的api可能很多人会用,但是Java Socket的参数可能很多人都不知道用来干嘛的,甚至都不知道有这些参数。

  backlog

  用于ServerSocket,配置ServerSocket的最大客户端等待队列。等待队列的意思,先看下面代码

  1. public class Main {  
  2.     public static void main(String[] args) throws Exception {  
  3.         int port = 8999;  
  4.         int backlog = 2;  
  5.         ServerSocket serverSocket = new ServerSocket(port, backlog);  
  6.         Socket clientSock = serverSocket.accept();  
  7.         System.out.println("revcive from " + clientSock.getPort());  
  8.         while (true) {  
  9.             byte buf[] = new byte[1024];  
  10.             int len = clientSock.getInputStream().read(buf);  
  11.             System.out.println(new String(buf, 0, len));  
  12.         }  
  13.     }  
  14. }

  这段测试代码在第一次处理一个客户端时,就不会处理第二个客户端,所以除了第一个客户端,其他客户端就是等待队列了。所以这个服务器最多可以同时连接3个客户端,其中2个等待队列。大家可以telnet localhost 8999测试下。

  这个参数设置为-1表示无限制,默认是50个最大等待队列,如果设置无限制,那么你要小心了,如果你服务器无法处理那么多连接,那么当很多客户端连到你的服务器时,每一个TCP连接都会占用服务器的内存,最后会让服务器崩溃的。

  另外,就算你设置了backlog为10,如果你的代码中是一直Socket clientSock = serverSocket.accept(),假设我们的机器最多可以同时处理100个请求,总共有100个线程在运行,然后你把在100个线程的线程池处理clientSock,不能处理的clientSock就排队,最后clientSock越来越多,也意味着TCP连接越来越多,也意味着我们的服务器的内存使用越来越高(客户端连接进程,肯定会发送数据过来,数据会保存到服务器端的TCP接收缓存区),最后服务器就宕机了。所以如果你不能处理那么多请求,请不要循环无限制地调用serverSocket.accept(),否则backlog也无法生效。如果真的请求过多,只会让你的服务器宕机(相信很多人都是这么写,要注意点)

  TcpNoDelay

  禁用纳格算法,将数据立即发送出去。纳格算法是以减少封包传送量来增进TCP/IP网络的效能,当我们调用下面代码,如:

  1. Socket socket = new Socket();    
  2. socket.connect(new InetSocketAddress(host, 8000));    
  3. InputStream in = socket.getInputStream();    
  4. OutputStream out = socket.getOutputStream();    
  5. String head = "hello ";    
  6. String body = "world\r\n";    
  7. out.write(head.getBytes());    
  8. out.write(body.getBytes());

  我们发送了hello,当hello没有收到ack确认(TCP是可靠连接,发送的每一个数据都要收到对方的一个ack确认,否则就要重发)的时候,根据纳格算法,world不会立马发送,会等待,要么等到ack确认(最多等100ms对方会发过来的),要么等到TCP缓冲区内容>=MSS,很明显这里没有机会,我们写了world后再也没有写数据了,所以只能等到hello的ack我们才会发送world,除非我们禁用纳格算法,数据就会立即发送了。

  SoLinger

  当我们调用socket.close()返回时,socket已经write的数据未必已经发送到对方了,例如

  1. Socket socket = new Socket();    
  2. socket.connect(new InetSocketAddress(host, 8000));    
  3. InputStream in = socket.getInputStream();    
  4. OutputStream out = socket.getOutputStream();    
  5. String head = "hello ";    
  6. String body = "world\r\n";    
  7. out.write(head.getBytes());    
  8. out.write(body.getBytes());   
  9. socket.close();



 这里调用了socket.close()返回时,hello和world未必已经成功发送到对方了,如果我们设置了linger而不小于0,如:

  1. bool on = true;  
  2. int linger = 100;  
  3. ....  
  4. socket.setSoLinger(boolean on, int linger)  
  5. ......  
  6. socket.close();

  那么close会等到发送的数据已经确认了才返回。但是如果对方宕机,超时,那么会根据linger设定的时间返回。

  UrgentData和OOBInline

  TCP的紧急指针,一般都不建议使用,而且不同的TCP/IP实现,也不同,一般说如果你有紧急数据宁愿再建立一个新的TCP/IP连接发送数据,让对方紧急处理。

  所以这两个参数,你们可以忽略吧,想知道更多的,自己查下资料。

  SoTimeout

  设置socket调用InputStream读数据的超时时间,以毫秒为单位,如果超过这个时候,会抛出java.net.SocketTimeoutException。

  KeepAlive

  keepalive不是说TCP的常连接,当我们作为服务端,一个客户端连接上来,如果设置了keeplive为true,当对方没有发送任何数据过来,超过一个时间(看系统内核参数配置),那么我们这边会发送一个ack探测包发到对方,探测双方的TCP/IP连接是否有效(对方可能断点,断网),在Linux好像这个时间是75秒。如果不设置,那么客户端宕机时,服务器永远也不知道客户端宕机了,仍然保存这个失效的连接。

  SendBufferSize和ReceiveBufferSize

  TCP发送缓存区和接收缓存区,默认是8192,一般情况下足够了,而且就算你增加了发送缓存区,对方没有增加它对应的接收缓冲,那么在TCP三握手时,最后确定的最大发送窗口还是双方最小的那个缓冲区,就算你无视,发了更多的数据,那么多出来的数据也会被丢弃。除非双方都协商好。

  以上的参数都是比较重要的Java Socket参数了,其他就不另外说明了。

posted @ 2012-02-16 11:39 顺其自然EVO 阅读(200) | 评论 (0)编辑 收藏

Java反射机制剖析:简单谈谈动态代理

     摘要: 通过《Java反射机制剖析:定义和API》和《Java反射机制剖析:功能以及举例》的学习,已经对反射有了一定的了解,这一篇通过动态代理的例子来进一步学习反射机制。  1、代理模式  代理模式就是为其他对象提供一种代理来控制对这个对象的访问。其实代理模式是在访问的对象时引入一定程度的间接性,这种间接性可以附加多种用途。  它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消...  阅读全文

posted @ 2012-02-13 13:35 顺其自然EVO 阅读(178) | 评论 (0)编辑 收藏

Java反射机制剖析:定义和API

  1、什么是Java反射机制

  Java的反射机制是在程序运行时,能够完全知道任何一个类,及其它的属性和方法,并且能够任意调用一个对象的属性和方法。这种运行时的动态获取就是Java的反射机制。其实这也是Java是动态语言的一个象征。

  用一句话来概括反射就是加载一个运行时才知道的类以及它的完整内部结构。

  2、为什么要有Java反射机制

  我们为什么要用Java的反射机制呢?

  我认为有两种:

  第一种:反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。

  第二种:在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象。

  一个生活中常看到的例子有助于理解我们为什么要用Java的反射机制:你进了一家饭店,你不知道他们都有那些菜,要多少钱。那么你第一件事情是干啥“服务员拿个菜单过来”,然后指着菜单说“我要这个,我要那个”。

  3、一起来看ReflectionAPI

  在生活中,我们使用一个未知的东西的时候总会用帮助来解决我们的使用问题,电视机有帮助,电脑有帮助,几乎所有的事物都携带着它的一本帮助,Java的反射机制也不例外。

  在JDK中有Reflection API的帮助,它主要说明了什么是Java反射机制,这种反射机制提供了什么样的属性和方法,进一步我们能够知道能够通过它完成什么样的工作

  下面咱就一起看看这部分的API。这些接口和类都位于lang包里。

  如图:

  接口:

  类:


  简单介绍一些类和接口的用法:

  1)Member成员是一种接口,反映有关单个成员(字段或方法)或构造方法的标识信息

  2)InvocationHandler是代理实例的调用处理程序实现的接口(这个接口的具体用法等到java反射机制剖析4着重介绍)

  3)Method提供一个类的方法的信息以及访问类的方法的接口。

  示例:

  1. import java.lang.reflect.Method;  
  2.  
  3. public class TestMethod {  
  4.  
  5.     /**  
  6.      * @param args  
  7.      * @throws Exception   
  8.      */ 
  9.     public static void main(String[] args) throws Exception {  
  10.         // TODO Auto-generated method stub  
  11.         Class classType=Class.forName(args[0]);  
  12.         Method methods[]=classType.getDeclaredMethods();  
  13.         for(int i=0;i<methods.length;i++){  
  14.             System.out.println(methods[i].toString());  
  15.         }  
  16.     }  
  17.  
  18. }

  4)Filed提供一个类的域的信息以及访问类的域的接口。

  示例:

  1. import java.lang.reflect.Field;  
  2.  
  3.  
  4. public class TestField {  
  5.  
  6.     /**  
  7.      * @param args  
  8.      * @throws Exception   
  9.      */ 
  10.     public static void main(String[] args) throws Exception {  
  11.         // TODO Auto-generated method stub  
  12.         Class classType=Class.forName(args[0]);  
  13.         Field[] fields = classType.getFields();  
  14.         for(int i=0;i<fields.length;i++){  
  15.         System.out.println(fields[i].toString());  
  16.         }  
  17.     }  
  18.  
  19. }

  5)Array类提供了动态创建和访问Java数组的方法。

  示例:

  1. import java.lang.reflect.Array;  
  2.  
  3.  
  4. public class TestArray {  
  5.  
  6.     public TestArray(){  
  7.           
  8.     }  
  9.     /**  
  10.      * @param args  
  11.      * @throws Exception   
  12.      */ 
  13.     public static void main(String[] args) throws Exception {  
  14.           
  15.         Class<?> classType = Class.forName("java.lang.String");  
  16.           
  17.         Object array = Array.newInstance(classType, 10);  
  18.           
  19.         Array.set(array, 5"hello");  
  20.           
  21.         String s = (String)Array.get(array, 5);  
  22.         System.out.println(s);  
  23.           
  24.     }  
  25.  
  26. }

  6)Proxy提供动态地生成代理类和类实例的静态方法(这个方法在java放射机制剖析4着重介绍)。

  其余的类和接口的使用方法详见API


posted @ 2012-02-09 12:00 顺其自然EVO 阅读(168) | 评论 (0)编辑 收藏

测试网站

http://www.jointest.org/forum.php?mod=viewthread&tid=9&extra=page%3D1

posted @ 2012-02-09 09:36 顺其自然EVO 阅读(142) | 评论 (0)编辑 收藏

仅列出标题
共394页: First 上一页 341 342 343 344 345 346 347 348 349 下一页 Last 
<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

导航

统计

常用链接

留言簿(55)

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜