和sun程序员的一次聊天

 

 

今天在 CSDN 的论坛上看了篇小说《谋划》 , 里面主人公和本人算是同行吧,搞互联网的。不过搞挨踢的文笔有那样很不错了,赞一下,各位闲下来时倒是可以拿来开心一下!在论坛里我看到有人贴出了高级 java ( 说里面技术大拿很多 ) ,本人写 java 也两年了,虽然是嫩了点,现在在空中网互联网事业部工作!反正有点闲,于是我加了这个群,没想到群主很特别,并没有第一次同意了我,而是以提问的方式拒绝了我。我也回答了几个问题,后来才得知这哥们 83 年的,在 sun 工作 2 年了,负责研发存储科技有关的 Java 软件,果真是牛人!今天也让我受教育了,特别是工作中有些细节容易被忽视,下面就问及的几个问题我也讨论下,也给自己提个醒钟:

 

1.       JAVA String 理论上最大长度是多少?

2.       JAVA 对象的串行化能存储在静态变量中吗?

3.       如果把 System.exit(0) 放在 catch 块中, finally 块是否还会被运行?

4.       Java System.gc() 是否能够启动垃圾回收?

5.       请说出 JNI 的很多缺点中的一个缺点?

6.       Java 里的 Annotation( 注释 ) ,能被子类继承吗?

。。。。。。

 

咋一看似乎也是很平常的问题,不过仔细一想有些问题给你 30 ( 不能用 google) 你未必能很快答出来:)

 

找了几篇和问题相关的不错的帖子

1.    JAVA String 理论上最大长度是多少?

要理解 javaString 的运作方式,必须明确一点:String 是一个非可变类(immutable )。什么是非可变类呢?简单说来,非可变类的实例是不 能被修改的,每个实例中包含的信息都必须在该实例创建的时候就提供出来,并且在对象的整个生存周期内固定不变。java 为什么要把String 设计为非可 变类呢?你可以问问 james Gosling :)。但是非可变类确实有着自身的优势,如状态单一,对象简单,便于维护。其次,该类对象对象本质上是线程安全的,不要求同步。此外用户可以共享非可变对 象,甚至可以共享它们的内部信息。(详见 《Effective javaitem 13 )。String 类在java 中被大量运用,甚至在class 文件中都有其身影,因此将其设计为简单轻便的非可变类是比较合适的。

一、创建。
   
好了,知道String 是非可变类以后,我们可以进一步了解String 的构造方式了。创建一个Stirng 对象,主要就有以下两种方式:

java 代码

  1. String str1 = new String("abc");    
  2. Stirng str2 = "abc";  

     虽然两个语句都是返回一个String 对象的引用,但是jvm 对两者的处理方式是不一样的。对于第一种,jvm 会马上在heap 中创建一个String 对 象,然后将该对象的引用返回给用户。对于第二种,jvm 首先会在内部维护的strings pool 中通过String equels 方法查找是对象池中是否存放有该String 对象,如果有,则返回已有的String 对象给用户,而不会在heap 中重新创建一个新的String 对象; 如果对象池中没有该String 对象,jvm 则在heap 中创建新的String 对象,将其引用返回给用户,同时将该引用添加至strings pool 中。注意:使用第一种方法创建对象时,jvm 是不会主动把该对象放到strings pool 里面的,除非程序调用 Stringintern 方法。看下面的例子:

java 代码

  1. String str1 = new String("abc"); //jvm  在堆上创建一个String 对象   
  2.   
  3.  //jvm  strings pool 中找不到值为“abc” 的字符串,因此   
  4.  // 在堆上创建一个String 对象,并将该对象的引用加入至strings pool   
  5.  // 此时堆上有两个String 对象   
  6. Stirng str2 = "abc";   
  7.   
  8.  if(str1 == str2){   
  9.          System.out.println("str1 == str2");   
  10.  }else{   
  11.          System.out.println("str1 != str2");   
  12.  }   
  13.   // 打印结果是 str1 != str2, 因为它们是堆上两个不同的对象   
  14.   
  15.   String str3 = "abc";   
  16.  // 此时,jvm 发现strings pool 中已有“abc” 对象了,因为“abc”equels “abc”   
  17.  // 因此直接返回str2 指向的对象给str3 ,也就是说str2str3 是指向同一个对象的引用   
  18.   if(str2 == str3){   
  19.          System.out.println("str2 == str3");   
  20.   }else{   
  21.          System.out.println("str2 != str3");   
  22.   }   
  23.  // 打印结果为 str2 == str3  

   再看下面的例子:

java 代码

  1. String str1 = new String("abc"); //jvm  在堆上创建一个String 对象   
  2.   
  3. str1 = str1.intern();   
  4. // 程序显式将str1 放到strings pool 中,intern 运行过程是这样的:首先查看strings pool   
  5. // 有没“abc” 对象的引用,没有,则在堆中新建一个对象,然后将新对象的引用加入至   
  6. //strings pool 中。执行完该语句后,str1 原来指向的String 对象已经成为垃圾对象了,随时会   
  7. // GC 收集。   
  8.   
  9. // 此时,jvm 发现strings pool 中已有“abc” 对象了,因为“abc”equels “abc”   
  10. // 因此直接返回str1 指向的对象给str2 ,也就是说str2str1 引用着同一个对象,   
  11. // 此时,堆上的有效对象只有一个。   
  12. Stirng str2 = "abc";   
  13.   
  14.  if(str1 == str2){   
  15.          System.out.println("str1 == str2");   
  16.  }else{   
  17.          System.out.println("str1 != str2");   
  18.  }   
  19.   // 打印结果是 str1 == str2   
  20.   

 

    为什么jvm 可以这样处理String 对象呢?就是因为String 的非可变性。既然所引用的对象一旦创建就永不更改,那么多个引用共用一个对象时互不影响。


二、串接(Concatenation )。
     java
程序员应该都知道滥用String 的串接操作符是会影响程序的性能的。性能问题从何而来呢?归根结底就是String 类的非可变性。既然 String 对象都是非可变的,也就是对象一旦创建了就不能够改变其内在状态了,但是串接操作明显是要增长字符串的,也就是要改变String 的内部状 态,两者出现了矛盾。怎么办呢?要维护String 的非可变性,只好在串接完成后新建一个String 对象来表示新产生的字符串了。也就是说,每一次执行串接操作都会导致新对象的产生,如果串接操作执行很频繁,就会导致大量对象的创建,性能问题也就随之而 来了。
   
为了解决这个问题,jdkString 类提供了一个可变的配套类,StringBuffer 。使用StringBuffer 对象,由于该类是可变的,串 接时仅仅时改变了内部数据结构,而不会创建新的对象,因此性能上有很大的提高。针对单线程,jdk 5.0 还提供了StringBuilder 类,在单线程环境下,由于不用考虑同步问题,使用该类使性能得到进一步的提高。

三、String 的长度
  
我们可以使用串接操作符得到一个长度更长的字符串,那么,String 对象最多能容纳多少字符呢?查看String 的源代码我们可以得知类String 中 是使用域 count 来记录对象字符的数量,而count  的类型为 int ,因此,我们可以推测最长的长度为 2^32 ,也就是4G
   
不过,我们在编写源代码的时候,如果使用 Sting str = "aaaa"; 的形式定义一个字符串,那么双引号里面的ASCII 字符最多只能有 65534 个。为什么呢?因为在class 文件的规范中, CONSTANT_Utf8_info 表中使用一个16 位的无符号整数来记录字符串的长度的,最多能表示 65536 个字节,而java class 文件是使用一种变体UTF-8 格式来存放字符的,null 值使用两个字节来表示,因此只剩下 65536 2 65534 个字节。也正是变体UTF-8 的原因,如果字符串中含有中文等非ASCII 字符,那么双引号中字符的数量会更少(一个中文字符占用三个字节)。 如果超出这个数量,在编译的时候编译器会报错。

 

2 JAVA 对象的串行化能存储在静态变量中吗?

Java 中对象的串行化( Serialization )和 transient 关键字

一、串行化的概念和目的

1.
什么是串行化
      
对象的寿命通常随着生成该对象的程序的终止而终止。有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。我们把对象的这种能记录自己的状态以便 将来再生的能力。叫作对象的持续性 (persistence) 。对象通过写出描述自己状态的数值来记录自己 ,这个过程叫对象的串行化 (Serialization -连续 ) 。串行化的主要任务是写出对象实例变量的数值。如果变量是另一对象的引用,则引用的对象也要串行化。这个过程是递归的,串行化可能要涉及一个复杂树结构的 单行化,包括原有对象、对象的对象、对象的对象的对象等等。对象所有权的层次结构称为图表 (graph)

2.
串行化的目的

Java
对象的单行化的目标是为 Java 的运行环境提供一组特性,如下所示:

1)
尽量保持对象串行化的简单扼要 ,但要提供一种途径使其可根据开发者的要求进行扩展或定制。
2)
串行化机制应严格遵守 Java 的对象模型 。对象的串行化状态中应该存有所有的关于种类的安全特性的信息。
3)
对象的串行化机制应支持 Java 的对象持续性。
4)
对象的串行化机制应有足够的 可扩展能力以支持对象的远程方法调用 (RMI)
5)
对象串行化应允许对象定义自身 的格式即其自身的数据流表示形式,可外部化接口来完成这项功能。



二、串行化方法
JDK1.1 开始, Java 语言提供了对象串行化机制 ,在 java .io 包中,接口 Serialization 用来作为实现对象串行化的工具 ,只有实现了 Serialization 的类的对象才可以被串行化。

Serializable
接口中没有任何的方法。当一个类声明要实现 Serializable 接口时,只是表明该类参加串行化协议,而不需要实现任何特殊的方法。下面我们通过实例介绍如何对对象进行串行化。

1.
定义一个可串行化对象

一个类,如果要使其对象可以被串行化,必须实现 Serializable 接口。我们定义一个类 Student 如下:
  class Student implements Serializable {
//1
首先定义了一个类 Student ,实现了 Serializable 接口
    int id;  
    String name; 
    int age; 
    transient String department; 
// Java
语言的关键字 [ 保留字 ] ,用来表示一个域不是该对象串行化的一部分。
    public Student(int id, String name, int age, String department) {
       this.id = id;
       this.name = name;
       this.age = age;
       this.department = department;
    }
}
2.
构造对象的输入/输出流

要串行化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。

java .io
包中,提供了 ObjectInputStream ObjectOutputStream 将数据流功能扩展至可读写对象 。在 ObjectInputStream 中用 readObject() 方法可以直接读取一个对象, ObjectOutputStream 中用 writeObject() 方法可以直接将对象保存到 输出流中。

package com.gwssi.study.pkg3;
import java .io.*;
public class ObjectSer {
    public static void main(String args[]) throws IOException,ClassNotFoundException{
       Student stu = new Student(101972040, "YaoMing", 27, "basketball");
       FileOutputStream fo = new FileOutputStream("data.ser");
       ObjectOutputStream so = new ObjectOutputStream(fo);
       try {
//2
通过对象输出流的 writeObject() 方法将 Student 对象保存到文件 data.ser
      so.writeObject(stu);
           so.close();
       } catch (IOException e) {
           System.out.println(e);
       }
       stu = null;
       FileInputStream fi = new FileInputStream("data.ser");
       ObjectInputStream si = new ObjectInputStream(fi);
       try {
//3
通过对家输入流的 readObjcet() 方法从文件 data.ser 中读出保存的 Student 对象
           stu = (Student)si.readObject();
           si.close();
       } catch (IOException ex) {
           System.out.println(ex);
       }
       //4
从运行结果可以看到,通过串行化机制,可以正确地保存和恢复对象的状态
       System.out.println("Student Info:");
       System.out.println("ID:" + stu.id);
       System.out.println("Name:" + stu.name);
       System.out.println("Age:" + stu.age);
       System.out.println("Dep:" + stu.department);

    }

}
  运行结果如下:


Student Info:
ID:101972040
Name:YaoMing
Age:27
Dep:null

在这个例子中,

1
首先定义了一个类 Student ,实现了 Serializable 接口

2
通过对象输出流的 writeObject() 方法将 Student 对象保存到文件 data.ser

3
通过对家输入流的 readObjcet() 方法从文件 data.ser 中读出保存的 Student 对象

4
从运行结果可以看到,通过串行化机制,可以正确地保存和恢复对象的状态

三、串行化的注意事项
1.
串行化能保存的元素

串行化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符都不能保存。

2.transient
关键字

对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的。例如一个 Thread 对象或一个 FileInputStream 对象 ,对于这些字段,我们必须用 transient 关键字标明,否则编译器将报措。

另外 ,串行化可能涉及将对象存放到 磁盘上或在网络上发达数据,这时候就会产生安全问题。因为数据位于 Java 运行环境之外,不在 Java 安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。应该在这些字段前加上 transient 关键字。

 

3. 如果把 System.exit(0) 放在 catch 块中, finally 块是否还会被运行?

这个就不用说了吧,当然不会执行了。如果 catch 换成别的,那就难说了。。。

 

4.       Java System.gc() 是否能够启动垃圾回收?

5.       finalize() 是由 JVM 自动调用的,你可以用 System.gc() ,但 JVM 不一定会立刻执行, JVM 感觉内存空间有限时,才会开始执行 finalize(), 至于新的对象创建个数和被收集个数不同是因为收集的对象只和 JVM 的垃圾收集策略有关。


1.
构造函数
要点 :
建器 (Constructor) 属于一种较特殊的方法类型 , 因为它没有返回值 . 这与 void 返回值存在着明显的区别。对于 void 返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自 动返回,而且根本不能有任何选择 . 若创建一个没有构件器的类 , 则编译器会自动创建一个默认构件器 .

2.finalize()
gc()

(1)
问题 :finalize() 函数是干嘛的 ?Java 不是有 Garbage Collection( 以下简称 gc) 来负责回收内存吗 ?
回答 :
gc
只能清除在堆上分配的内存 ( java 语言的所有对象都在堆上使用 new 分配内存 ), 而不能清除栈上分配的内存(当使用 JNI 技术时 , 可能会在栈上分配内 , 例如 java 调用 c 程序,而该 c 程序使用 malloc 分配内存时) . 因此 , 如果某些对象被分配了栈上的内存区域 , gc 就管不着了 , 对这样的对象进行 内存回收就要靠 finalize().
举个例子来说 , java 调用非 java 方法时(这种方法可能是 c 或是 c++ 的) , 在非 java 代码内部也许调用了 c malloc() 函数来分配内存,而且除非调用那个了 free() 否则不会释放内存 ( 因为 free() c 的函数 ), 这个时候要进行释放内存的工作 ,gc 是不起作用的 , 因而需要在 finalize() 内部的一个固有方法 调用它 (free()).
finalize
的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用 finalize() ,而且只有在下一次垃圾收集过程中,才会真正回收对象的内存 . 所以如果使用 finalize() ,就可以在垃圾收集期间进行一些重要的清除或清扫工作 .

(2)
问题 :finalize() 在什么时候被调用 ?
回答 :

有三种情况

1.
所有对象被 Garbage Collection 时自动调用 , 比如运行 System.gc() 的时候 .
2.
程序退出时为每个对象调用一次 finalize 方法。
3.
显式的调用 finalize 方法

除此以外 , 正常情况下 , 当某个对象被系统收集为无用信息的时候 ,finalize() 将被自动调用 , 但是 jvm 不保证 finalize() 一定被调用 , 也就是说 ,finalize() 的调用是不确定的 , 这也就是为什么 sun 不提倡使用 finalize() 的原因 .


3. this
要点 :
this
关键字只能在方法中使用 , 它能为调用该方法的对象提供相应的句柄 , 使得同一个类产生的不同对象实例在调用同一方法的时候 , 系统能判断出是哪一个对象在进行调用 .
比如 :
MyObject a=new MyObject();
MyObject b=new MyObject();
a.f();// (3)
b.f();// (4)
编译器在编译的时候 , 实际上是将 (3),(4) 句解释为
MyObject.f(a);
MyObject.f(b);
, 这样就将调用了该方法的对象的信息传到了方法中 , 也就是传给了 this, 就可以通过 this 表示调用该方法的对象实例 .

this 的概念还可以解释为什么在静态方法中不能调用非静态方法和元素 , 这是因为静态方法中没有 this, 也就是说我们不能获得调用该方法的对象的句柄 . 既然找不到这个对象实例 , 我们又怎么能够在其中调用对象实例的方法和元素呢 ?

为什么静态方法没有 this ? 用静态方法的概念可以来理解这个问题 . 静态方法是类方法 , 是所有对象实例公用的方法 . 它不属于某一个具体的对象实例 , 因此 也无法用 this 来体现这个实例 . 这和非静态方法是不一样的 . 打个比方 , 在一个局域网内的几个用户每个人都有一台客户机 , 但都访问一台公共的服务器 . 对于 每台客户机来说 , 它的 this 就是使用它的用户 . 而对于服务器来说 , 它没有 this, 因为它是大家公用的 , 不针对某一个具体的客户 .

4.
对象初始化
要点 :
1.
对象只有在创建的时候 , 需要使用它的时候才进行初始化 , 否则永远都不会初始化 .
2.
对象进行初始化是有一定顺序的 , 无论在定义的时候各个成员的摆放位置如何 . 首先是静态成员和对象 , 然后是非静态成员和对象 , 最后才运行构造器 .
3.
静态成员和对象有且只有一次初始化过程 , 这个过程发生在第一次创建对象或者第一次使用类的静态成员和对象的时候 .

以一个名为 Dog 的类为例 , 它的对象实例初始化过程如下:
(1)
类型为 Dog 的一个对象首次创建时,或者 Dog 类的 static 方法/ static 字段首次访问时, Java 解释器必须找到 Dog.class (在事先设好的类路径里搜索)。
(2)
找到 Dog.class , 它的所有 static 初始化模块都会运行。因此, static 初始化仅发生一次 ?D?D Class 对象首次载入的时候。
(3)
创建一个 new Dog() 时, Dog 对象的构建进程首先会在内存堆( Heap )里为一个 Dog 对象分配足够多的存储空间。
(4)
这种存储空间会清为零,将 Dog 中的所有基本类型设为它们的默认值
(5)
进行字段定义时发生的所有初始化都会执行。
(6)
执行构建器。正如第 6 章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候

5.
数组的初始化
数组包括基本数据类型数组和对象数组 , 其中对于对象数组的初始化 , 经常会出现 "Exception" 错误 . 比如下面的程序

问题代码如下 :

public userInfo[] getUsersInfo() {

userInfo[] usersInfo=null;

if (users.size()!=0) {
usersInfo=new userInfo[users.size()];

for(int i=0;i< usersInfo.length;i++) {
//+-------------------
出问题的地方 -----------------
usersInfo[i].name=((User)(users.elementAt(i))).name;
usersInfo[i].type=((User)(users.elementAt(i))).type;
usersInfo[i].userID=((User)(users.elementAt(i))).userID;
//+-------------------
出问题的地方 -----------------
}
System.out.println("here");
return usersInfo;
}else {
return null;
}
}



其中 userInfo 的定义为

class userInfo{
userInfo(String name,int type,int userID){
this.name=name;
this.type=type;
this.userID=userID;
}
String name;
int type;
int userID;
}



运行到程序中标出的问题区域时 , 系统显示 NullPointerException, 为什么会这样呢 ?

这是因为 ,Java 在定义数组的时候
usersInfo=new userInfo[users.size()];
并没有给数组元素分配内存 , 它只是一个句柄数组 , 数组中的对象还没有初始化 . 因此数组中的每个对象都需要 new 之后才可以访问 . 例如 :
A[] a=new A[2];
for(int i=0;i<2;i++)
a[i] = new A();
这样才能 a[i].someMethod()

因此上面的程序应该改为

public userInfo[] getUsersInfo() {

userInfo[] usersInfo=null;

if (users.size()!=0) {
usersInfo=new userInfo[users.size()];

for(int i=0;i< usersInfo.length;i++) {
//+-------------------
修改的地方 -----------------
usersInfo[i]=new userInfo(((User)(users.elementAt(i))).name,
((User)(users.elementAt(i))).type,
((User)(users.elementAt(i))).userID);
}
//+-------------------
修改的地方 -----------------
return usersInfo;
}else {
return null;
}
}


没问题了简单来讲, finalize() 是在对象被 GC 回收前会调用的方法,而 System.gc() 强制 GC 开始回收工作纠正,不是强制,是建议,具体 执行要看 GC 的意思简单地说,调用了 System.gc() 之后, java 在内存回收过程中就会调用那些要被回收的对象的 finalize() 方法。

 

5: 请说出 JNI 的很多缺点中的一个缺点?

使用比较麻烦 , 需要对已有的 DLL 进行封装 , 需要对 C/C++ 比较了解 ;

破坏了程序的可移植性;

影响程序的安全性

。。。

 

6 Java 里的 Annotation( 注释 ) ,能被子类继承吗?

有关 Annotation 的继承说明:

1JDK 文档中的说明是:只有在类上应用的Annotation 才能被继承,而实际应用时的结果是:除了类上应用的Annotation 能被继承外,没有被重写的方法的Annotation 也能被继承。

2 、要注意的是:当方法被重写后,Annotation 将不会被继承

3 、要使得Annotation 被继承,需要在Annotation 中加标识@Inherited ,并且如果要被反射应用的话,就需要还有个@Retention(RetentionPolicy.RUNTIME) 标识

4Annotation 的继承不能应用在接口上

。。。

 

之后我们还聊了很多,他在美国研发的现状,受金融风暴的影响等等。。。

 

几个看似普通的题目,几分钟的对话,让我对这个和自己年龄相仿的sun程序员刮目相看,从中看到了人家做事的态度、学习的习惯和超脱的能力。。。

 

以后在开发的过程中也好,做人的生活中也好,给自己提个醒钟,多注意身边的细节。。。

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值