集合类参数的传递

原创 2007年09月17日 11:02:00


对于传参,从计算机的本质而言是传值.因为从数据模型而言计算机只认识数学值.但是不同的数学值代表的意义不同,有的数学值表示的是另一个数据的地址.所以根据这个数学值能访问到它表示的数据,我们就把这样的操作称为传址.其实就是某个数据所在的地址的数学表示.其本质还是传值.

 从应用层而言我们所说的地址当然是指虚拟地址.而对于中间语言(java的byteCode/.NET的IL)而言是托管地址.我们不必关心实际的内存地址如何和它们对应,这由作业系统和应用环境来决定,你想关心也关心不了.我们要关心的是理解作业系统和应用环境提供给我们的可访问地址的内存布局.

对于集合类参数,如果传入方法后在方法外重新赋值参数本身,这和其它引用参数一样不会影响方法内的参数.但如果对集合中元素重新赋值则改变了方法内的集合中的元素,因为方法外和方法内的集合就是本身是同一对象.

class MyRun{
    public static void exec(String[] args){
        for(int i=0;i<args.length;i++){
            try{
                check(args[ i ]);
            }catch(Exception e){}
            invoke(args[ i ]);
        }
    }
    static void check(String str) throws Exception{
        if(str.equals("s2")) throw new Exception("error1");
    }
    static void invoke(String str){
        System.out.println("执行的语句是"+str);
    }
}
这段程序设计是否有错?如果有错,如何修改?

这是我在bea论坛上贴出来的一段程序,最初没有一个人能说明有什么错,更别说如何修改.(我知道有很多水平很高的高手根本不去bea论坛所以没有看到这个问题)
更可悲的是我把问题展示了很多根本看不懂的人说我在胡说八道,这就是中国程序员的现状,他没有能力理解和不知道的东西都叫胡说八道,当他上小学时他说初中,高中,大学,研究生的知识是胡说八道的.

这个问题的展示很简单:
当你设计了上面的类以后, 那么我作为调用者,我可以任何方式调用你的类,你都应该是安全的.
好,我现在这样调用:

先设计一个用来改变数据的线程:
class ModifyThread extends Thread{
    private String[] arr;
    public ModifyThread(String[] arr){
        this.arr = arr;
    }
    public void run(){
        try{
        Thread.currentThread().sleep(20);
        }catch(Exception e){}
        arr[0] = "s2";
    }
}
为了说明问题,我在你设计的类中插入一段sleep来模拟线程运行到那里时被切换到其它线程运行,然后又切换回来到本线程运行的情况:

class MyRun{
    public static void exec(String[] args){
        for(int i=0;i<args.length;i++){

            try{
                check(args[  i  ]);
                //为了说明问题,在这儿sleep(100)来模拟运行到这里时线切换到其它线程去运行
                  Thread.currentThread().sleep(100);
            }catch(Exception e){}
            //然后又回到这个这线程继续运行.
            invoke(args[  i  ]);

        }
    }
    static void check(String str) throws Exception{
        if(str.equals("s2")) throw new Exception("error1");
    }
    static void invoke(String str){
        System.out.println("执行的语句是"+str);
    }
}
然后调用:
public class Main {

    /** Creates a new instance of Main */

    public static void main(String[] args) {
        // TODO code application logic here
        String[] strs = {"s1"};
        new ModifyThread(strs).start();
        MyRun.exec(strs);
       
    }

}
试试看,我利用一个辅助线程就把s2传进去执行了.

好,有人说要同步:
class MyRun{
    public static void exec(String[] args){
        for(int i=0;i<args.length;i++){
            synchronized(args){
            try{
                check(args[  i  ]);
                //为了说明问题,在这儿sleep(100)来模拟运行这里时线切换到其它线程去运行了.
                //然后又回到这个这线程继续运行.
                Thread.currentThread().sleep(100);
            }catch(Exception e){}
            invoke(args[  i  ]);
            }
        }
    }
    static void check(String str) throws Exception{
        if(str.equals("s2")) throw new Exception("error1");
    }
    static void invoke(String str){
        System.out.println("执行的语句是"+str);
    }
}

再调用看看,s2仍然越过了check.因为同步只能保证明多个MyRun.exec()方法在执行时只有一个线程能访问方法内的args数组,根本无法保证从方法外修改strs, 因为方法外的代码没有和方法内共同竞争同一对象锁,.我用sleep来模拟线程间执行的切换.其实只要我用足够多的ModifyThread线程在不同时刻运行起来,和MyRun.exec一起运行,产生这种切换的可能性就非常大.简单说我只要用足够多的辅助线程就能绕过你设计的check.

其实要解决问题非常容易:
class MyRun{
    public static void exec(String[] args){
        //将传入变量复制为方法内的本地变量,打断与方法外的联系.
        //然后只能本地变量操作
        String[]temp = new String[args.length];
        System.arraycopy(args, 0,temp, 0, args.length);
        for(int i=0;i<temp.length;i++){
            //synchronized(temp){
           //然后根本不需要同步.
            try{
                check(temp[  i  ]);
                //为了说明问题,在这儿sleep(100)来模拟运行这里时线切换到其它线程去运行了.
                //然后又回到这个这线程继续运行.
                Thread.currentThread().sleep(100);
            }catch(Exception e){}
            invoke(temp[  i  ]);
            //}
        }
    }
    static void check(String str) throws Exception{
        if(str.equals("s2")) throw new Exception("error1");
    }
    static void invoke(String str){
        System.out.println("执行的语句是"+str);
    }
}

如果你还不能理解或者不相信我,我们来看看JDK(1.6)是如何处理的.Runtime.exec最终调用了ProcessBuilder的start()方法.在ProcessBuilder中,外部命令可以通过command(List<String> command)这样的方法将一个List传进来,而本地用全局的List command保存也就是command(List<String> command)的实现是: this.command = command;
这样外部传入一个保存了多个命令的List后,从外部仍然可以访问方法内的List中的内容.所以在start()方法中JDK这样处理,而且加上了这样的注释:

// Must convert to array first -- a malicious user-supplied
// list might try to circumvent the security check.
String[] cmdarray = command.toArray(new String[command.size()]);
然后对方法内的cmdarray 进行操作,用户修改外部的list就不会影响到cmdarray 中的元素.
这不仅是安全的问题.设计一个不变类,如果传入参数是数据结构的容器类,那就不能保证是不变类,但只要在处理前复制为方法内的局部变量就可以保证类的不变性.
 

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

Gephi作网络图

作者:金良(golden1314521@gmail.com) csdn博客:http://blog.csdn.net/u012176591 下面是我根据博客园的粉丝关系数据做出的图。博客园的博客用户有...

UGG Fluff Flip Flop Ugg Fluff

,Roxy Ugg BootUgg Fluff Flip FlopSlippers (Black)Fluff black sheepskin flip flop style slippers with...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

Gephi学习笔记-画出图形基本流程

1、数据准备       这里准备的是csv格式的数据,没有用数据库导入等方式。       格式如下图所示,这里表示的是一个有权有向图,Source为源节点,Target为目标节点,Weight为权...

一个UUID生成算法的C语言实现 --- WIN32版本

一个UUID生成算法的C语言实现——WIN32版本 cheungmine2007-9-16 根据定义,UUID(Universally Unique IDentifier,也称GUID)在时间和空间都...

Gephi丰富的插件一瞥

Gephi的可扩展性是值得称赞的。 所需插件可以从这里下载:https://marketplace.gephi.org/下载的文件是一个以nbm为后缀的gephi插件文件,在gephi的主界面的工具...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)