精华内容
下载资源
问答
  • 【Java】基础知识部分

    2021-05-28 16:19:34
    【Java】基础知识部分 一、数组 单个数组内存分配图 多个数组内存分配图 多个数组指向相同地址 这种情况下,多个数组指向同一...再次使用的时候会报错:空指针异常 二、内部类、抽象类、包装类、修饰符 内部类 在一

    一、数组

    单个数组内存分配图

    image-20210424235050443

    多个数组内存分配图

    image-20211018155323251

    多个数组指向相同地址

    image-20211018155340856

    这种情况下,多个数组指向同一个地址值。

    中间一行的赋值操作是将arr的地址值赋值给arr2,如果这个时候针对arr2进行操作,那么也就相当于是对arr进行操作,本质上指向的是同一个数组。所以无论操作arr还是arr2,结果上没有本质上的区别。

    数组空指针异常

    image-20210424235746162

    如果数组被赋值为null,那么将找不到数组本身存放的堆内存地址。再次使用的时候会报错:空指针异常

    二、内部类、抽象类、包装类、修饰符

    内部类

    在一个类中定义一个类,类中被定义的类就是内部类

    内部类的访问特点

    • 内部类可以直接访问外部类的成员,包括私有

    • 外部类要访问内部类的成员,必须创建对象

    public class Outer {
        private int num = 20;
        public class Inner {
            public void show() {
                System.out.println(num);
            }
        }
        private void method() {
            Inner inner = new Inner();
            inner.show();
        }
    }
    

    成员内部类

    根据内部类的位置不同,可以分为两种:

    • 在类的成员位置:成员内部类
    • 在类的局部位置:局部内部类

    成员内部类如何使用呢?两种方式:

    一、将内部类的权限名定义为public,之后创建内部类

    public class Outer {
        private int num = 20;
        public class Inner {
            public void show() {
                System.out.println(num);
            }
        }
    }
    
    public static void main(String[] args) {
        // 创建对象调用内部类方法
        Outer.Inner oi = new Outer().new Inner();
        oi.show();
    }
    

    二、如果Inner内部类的权限名不是public,则上述方法失效,那么如何调用呢?

    在外部类内创建新的方法,创建内部类,调用方法;外界直接创建外部类,并调用该方法即可

    public class Outer {
        private int num = 20;
        private class Inner {
            public void show() {
                System.out.println(num);
            }
        }
        public void method() {
            Inner i = new Inner();
            i.show();
        }
    }
    
    public static void main(String[] args) {
        // 创建对象调用内部类
        /*Outer.Inner oi = new Outer().new Inner();
        oi.show();*/
        Outer o = new Outer();
        o.method();
    }
    

    局部内部类

    局部内部类就是在方法体中的类,所以外界是无法使用的,需要在方法中创建该局部内部类的对象,通过调用对象内部的方法使用

    该类可以访问外部的成员,也可以访问方法内的局部变量

    public class Outer {
        private int num = 10;
        public void method() {
            class Inner {
                int num2 = 20;
                public void show() {
                    System.out.println(num);
                    System.out.println(num2);
                }
            }
            // 直接在方法内部创建对象调用局部内部类的方法
            Inner i = new Inner();
            i.show();
        }
    }
    
    public static void main(String[] args) {
        Outer o = new Outer();
        o.method();
    }
    

    匿名内部类

    前提:存在一个类或者一个接口,这里的类可以是具体类也可以是抽象类

    格式:

    new class/interface() {
        // Override method()
    };
    

    本质是***一个继承了该类或实现了该接口的子类匿名对象***

    步骤一:有一个类或者接口

    public interface Inter {
        void show();
    }
    

    步骤二:创建相关的类

    public class Outer {
        public void method() {
            /*new Inter() {
                @Override
                public void show() {
                    System.out.println("匿名内部类方法执行");
                }
            };
            这样写仅仅是个对象,下面的写法才是对象调用方法:
            new Inter() {
                @Override
                public void show() {
                    System.out.println("匿名内部类方法执行");
                }
            }.show();*/
    
            // 由于该匿名内部类实现的是 Inter 接口,我们可以用接口类型来接受这个匿名内部类
            Inter i = new Inter() {
                @Override
                public void show() {
                    System.out.println("匿名内部类方法执行");
                }
            };
    
            i.show();
            i.show();
        }
    }
    

    步骤三:测试

    public class Test {
        public static void main(String[] args) {
            Outer o = new Outer();
            o.method();
        }
    }
    

    输出结果:

    匿名内部类方法执行
    匿名内部类方法执行

    匿名内部类在开发中的使用

    仅使用一次,创建接口操作类的对象,调用接口操作方法,方法的参数是接口

    不想创建接口实现类的情况,并且只想用一次,就可以使用匿名内部类

    抽象类

    Java中,没有方法体的方法应该被定义为抽象方法;类中如果有抽象方法,则应该定义为抽象类

    注意事项:

    • 抽象类中的方法不一定是抽象方法,但是抽象方法所属的类一定要是抽象类,抽象类中一定存在抽象方法
    • 抽象类中的子类要么是抽象类,要么重写抽象类中的所有抽象方法
    • 抽象类不能实例化,但是抽象类可以通过子类对象进行实例化,这叫抽象类多态

    抽象类的成员特点

    抽象类中可有成员变量、成员方法、构造方法

    • 成员变量
      • 可以是变量,也可以是常量
    • 成员方法
      • 可以有抽象方法:限定子类必须完成某些动作
      • 可以有非抽象方法:提高代码的复用性
    • 构造方法
      • 有构造方法,但是不能实例化
      • 抽象方法的实例化是通过子类的对象进行实例化的,子类对象对于父类数据的初始化要使用到这些构造方法
    public abstract class Animal {
        // 抽象类中可以包含成员变量
        private int age = 20;
        private final String city = "北京";
    
        // 因为抽象类是通过子类对象进行实例化的,所以子类对象在使用构造方法创建时,
        // 会隐式的调用父类的构造方法,也就是抽象类的构造方法
        public Animal() {}
    
        public Animal(int age) {
            this.age = age;
        }
    
        public void show() {
            age = 40;
            System.out.println(age);
            System.out.println(city);
        }
    
        /*public void eat() {
            System.out.println("吃东西");
        }*/
        // 抽象方法
        public abstract void eat();
        // 抽象类中可以有具体的方法
        public void sleep() {
            System.out.println("睡觉");
        }
    }
    

    包装类

    image-20210406145151828

    基本数据类型使用虽然非常方便,但是没有对应的方法来操作这些数据。所以我们可以使用包装类将这些基本数据类型进行一定的封装,把基本类型的数据包装起来,这就是包装类。

    在包装类中可以定义一些方法,用来操作基本类型的数据。

    装箱与拆箱

    image-20210406150512203

    修饰符

    Java中的修饰符分为两大类:权限修饰符、状态修饰符

    权限修饰符

    image-20210509204638596

    权限修饰符,修饰的是访问的权限,指的是在同一个module中的不同类中的访问权限

    状态修饰符

    final(最终态)

    可以修饰成员方法、成员变量、类

    • final修饰方法,表明该方法是最终方法,不能被重写
    • final修饰变量,表明该变量是最终变量,不能被再次赋值
    • final修饰类,表明该类是最终类,不能被继承

    final修饰局部变量:

    • final修饰基本数据类型变量,变量的数据值不能发生改变
    • final修饰引用数据类型变量,变量的地址值不能发生改变,但是地址里的内容是可以发生改变的
    static(静态)

    可以修饰成员方法、成员变量

    • 被类的所有对象共享——这也是我们判断是否使用static关键字的条件
    • 可以通过类名.变量名调用(也可以使用对象名调用),推荐使用类名调用
    • 静态成员方法只能访问静态成员

    三、继承、多态

    继承

    继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法

    继承的利弊

    • 优点:
      • 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
      • 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
    • 缺点:
      • 继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时,子类实现也不得不跟着变化,削弱了子类的独立性

    继承使用的情况

    什么时候使用继承?

    当类A是类B的”一种/一个“时,就可以使用继承关系。

    继承中成员变量访问

    • 在子类方法中访问变量
      • 子类局部范围找
      • 子类成员范围找
      • 父类成员范围找
      • 如果都没有就报错(不考虑父类的父类)

    super与this关键字的使用

    • super:代表父类存储空间的标识(可以理解为父类对象引用)
    • this:代表本类对象的引用

    image-20210509194539063

    继承中成员方法的访问

    继承中的成员方法的访问:(子类访问成员方法)

    • 子类成员范围找
    • 父类成员范围找
    • 如果找不到就报错(不考虑父类的父类)
    public static void main(String[] args) {
        Zi zi = new Zi();
        // 调用子类成员方法
        zi.method();
        // 调用子类和父类中的重名无参方法
        // 真正调用的是子类中的同名方法
        zi.show();
        // 调用父类中的方法,需要在子类中的同名方法中添加 super.show();
    }
    

    继承中构造方法的访问特点

    继承中,关于构造方法的访问:

    • 子类中所有的构造方法都会默认访问父类中的无参构造方法

    • 原因:子类继承自父类,在子类调用父类时可能会用到父类的数据,所以在子类进行初始化的时候需要先对父类进行初始化操作

    • 因为子类会继承父类的数据,可能还会使用父类的数据。所以在子类初始化之前需要先完成父类数据的初始化

    • 每一个子类构造方法的第一句默认都是super();

    如果父类中没有无参构造方法,只有带参构造方法,解决方法:

    • 通过使用super关键字显式的调用父类的带参构造方法
    • 在父类中自己提供一个无参构造方法(推荐使用)
    public static void main(String[] args) {
        /*
        * 父类无参构造方法被调用
        * 子类无参构造方法被调用
        * */
        Zi z1 = new Zi();
        /*
        * 父类无参构造方法被调用
        * 子类带参构造方法被调用
        * */
        Zi z2 = new Zi(20);
    }
    

    super中的内存图

    image-20210509201403215

    方法重写

    子类中出现了和父类中一样的方法声明

    当子类中需要父类的功能,而功能主体子类有自己特有内容,可以重写父类中的方法。这样既沿袭了父类中的功能,又定义了子类特有的功能

    方法重写时最好添加@Override注解,可以帮忙检查方法重写的方法声明是否正确

    注意事项

    • 父类中的私有方法子类不能重写(父类中的私有成员子类是不能被继承的)
    • 子类重写父类方法,子类的访问权限不能更低。例如:父类成员方法为默认,子类重写方法权限为默认或比默认更高(protected,public)
      • 方法访问权限:public > protected > 默认 > private

    继承的注意事项

    Java中的继承只支持单继承,不支持多继承(一个类继承自多个类,不允许)

    继承支持多级继承,子类继承自父类,父类继承自父类的父类(爷爷类)这样的继承是合法的

    多态

    多态定义的时候:左父右子

    多态中成员访问

    成员变量:编译看左边,执行看左边

    成员方法:编译看左边,执行看右边

    成员方法和成员变量执行不同的原因:因为成员方法有重写,而成员变量没有

    底层上的解释就是成员变量属于前期绑定(静态绑定,程序编译期的绑定),成员方法属于后期绑定(动态绑定,程序运行期的绑定)。

    重载属于前期绑定,重写属于后期绑定。

    也就是说:多态的编译是否能通过,要看父类中是否有相关的变量和方法;执行则要看是变量还是方法

    多态的利弊

    • 多态的好处:提高了程序的扩展性

    • 多态的弊端:不能使用子类的特有功能

    • 定义多态方法的时候,使用父类型作为参数,使用具体的子类型进行操作

    实际使用如下:

    ①创建Animal类

    public class Animal {
        public void eat() {
            System.out.println("动物吃东西");
        }
    }
    

    ②创建Dog类和Cat类

    public class Cat extends Animal{
        public void eat() {
            System.out.println("猫吃老鼠");
        }
    }
    
    public class Dog extends Animal{
        public void eat() {
            System.out.println("狗吃骨头");
        }
    
        public void gatekeeper() {
            System.out.println("狗看门");
        }
    }
    

    ③创建测试主类

    public class AnimalDemo {
        public static void main(String[] args) {
            Animal a = new Cat();
            a.eat();
    
            Animal b = new Dog();
            // b.gatekeeper();
            b.eat();
        }
    }
    

    这个时候我们调用gatekeeper方法则会报错,因为gatekeeper方法是Dog类独有的方法。多态的弊端此时体现出来了:因为在Animal父类中没有定义gatekeeper方法,那么在使用多态的时候就不能调用到子类的独有方法。

    解决方法:①在Animal类中添加Dog的特有方法,那这样Cat类也能够调用gatekeeper方法,本质上二者相悖了。

    public void gatekeeper() {
        System.out.println("动物看家护院");
    }
    

    所以说,多态的弊端就是不能调用子类的特有方法。

    ②向下转型,将Animal类定义的时候的b对象(Dog)转型为Dog本身的类型。这样转型之后其实也与多态定义的时候的方法不相符,违背了多态本身的定义。

    ((Dog) b).gatekeeper();
    

    四、接口、泛型

    接口Interface

    接口就是一种公共的规范标准,Java中的接口更多体现在对行为的抽象

    接口使用interface关键字来创建;接口的使用是通过类来实现该接口实现的

    public interface Jumping {
        public abstract void jump();
    }
    

    接口不能实例化,要想使用需要通过一个类来实现该接口,通过实现类对象来实例化(与抽象类在这一方面类似),叫做接口多态

    接口的成员特点

    • 成员变量
      • 只能是常量,默认由public static final修饰,不能进行二次赋值
    • 成员方法
      • 只能是抽象方法,默认由public abstract修饰,不能是非抽象方法
    • 构造方法
      • 接口中没有构造方法,因为接口主要是对行为进行抽象,没有具体存在
      • 一个类如果没有父类,则默认继承自Object类
    public interface Inter {
        public int num = 20; // 接口中的成员变量默认是被 static final 修饰
        public final int num2 = 30;
        public static final int num3 = 40;
    
        public abstract void method();
        void show();
    
        /* 接口中不能有构造方法和非抽象方法的
        public Inter() {}
        public void show() {}*/
    }
    

    泛型Generic

    image-20210424221805237

    泛型的使用可以使一些集合的使用中可能出现的类型错误,由运行期错误转换为编译期错误,编码的时候更加安全。

    ①把运行期间的问题提前到了编译期

    ②避免了强制类型转换

    泛型类

    image-20210424222721257

    使用步骤:

    ①创建泛型类Generic类

    public class Generic<T> {
        private T t;
    
        public T getT() {
            return t;
        }
    
        public void setT(T t) {
            this.t = t;
        }
    }
    

    ②使用泛型类

    public class GenericDemo {
    
        public static void main(String[] args) {
            Student student = new Student();
            student.setStuName("张三");
            student.setStuAge(18);
            System.out.println(student.getStuName());
            System.out.println(student.getStuAge());
    
            Generic<String> g1 = new Generic<String>();
            g1.setT("李四");
            System.out.println(g1.getT());
    
            Generic<Integer> g2 = new Generic<Integer>();
            g2.setT(20);
            System.out.println(g2.getT());
        }
    
    }
    

    对比普通类(student),泛型类在使用的时候更加简便,不需要过多的创建set和get方法

    泛型方法

    在上述的例子中,我们使用泛型方法需要多次创建新的对象,使用中还是比较繁琐。为了能够创建一次对象,多次调用不同的参数的同一个方法,我们可以使用泛型方法。

    定义格式如下

    public class GenericFunction {
        public <T> void show(T t) {
            System.out.println(t);
        }
    }
    

    使用方法

    GenericFunction g = new GenericFunction();
    g.show("雨下一整晚Real");
    g.show(20);
    g.show(true);
    

    打印输出结果如下所示:

    雨下一整晚Real
    20
    true

    泛型接口

    我们定义一个泛型接口

    public interface GeneticInterface<T> {
        void show(T t);
    }
    

    定义接口的实现类

    public class GenericInterfaceImpl<T> implements GeneticInterface<T>{
        @Override
        public void show(T t) {
            System.out.println(t);
        }
    }
    

    测试运行类

    GeneticInterface<String> geneticInterface1 = new GenericInterfaceImpl<String>();
    geneticInterface1.show("Real");
    
    GeneticInterface<Integer> geneticInterface2 = new GenericInterfaceImpl<Integer>();
    geneticInterface2.show(21);
    
    GeneticInterface<Boolean> geneticInterface3 = new GenericInterfaceImpl<Boolean>();
    geneticInterface3.show(true);
    

    运行结果如下

    Real
    21
    true

    类型通配符

    image-20210424230018722

    public class GenericDemo {
        public static void main(String[] args) {
            List<?> list1 = new ArrayList<Object>();
            List<?> list2 = new ArrayList<Number>();
            List<?> list3 = new ArrayList<Integer>();
            /*这三个类是继承关系,按照继承顺序编写的*/
            System.out.println("--------");
    
            /*类型通配符上限*/
            // List<? extends Number> list4 = new ArrayList<Object>();
            List<? extends Number> list5 = new ArrayList<Number>();
            List<? extends Integer> list6 = new ArrayList<Integer>();
    
            /*类型通配符下限*/
            List<? super Number> list7 = new ArrayList<Object>();
            List<? super Number> list8 = new ArrayList<Number>();
            // List<? super Number> list9 = new ArrayList<Integer>();
    
        }
    }
    

    在上述的代码中,添加注释的行是错误的。根据上限和下限的定义,我们可以得出super和extends的使用。

    可变参数

    要想实现多个数字之和,这种方法的实现,需要用到可变参数

    如果为每一个数量的数求和编写一个方法,那么工作量将会变得非常大。这个时候我们就可以用到可变参数

    使用如下:

    public static void main(String[] args) {
        System.out.println(sum(10, 20));
        System.out.println(sum(10, 20, 30));
        System.out.println(sum(10, 20, 30, 40));
    }
    
    static int sum(int... a) {
        int sum = 0;
        for (int i : a) {
            sum += i;
        }
        return sum;
    }
    

    其中,a是一个数组类型的数据。我们求和的时候直接遍历数组求和即可。

    如果sum方法有多个参数,那么可变参数应该放在后面

    image-20210424231843121

    如果调换二者的顺序,则不能通过编译。

    可变参数的使用

    image-20210424233045153

    public static void main(String[] args) {
        List<String> list = Arrays.asList("Hello", "World", "java");
        // UnsupportedOperationException
        // list.add("java EE");
        // list.remove("java");
        list.set(2, "java EE");
        System.out.println(list);
    
        List<String> stringList = List.of("Hello", "World", "java");
        // UnsupportedOperationException
        // stringList.add("java EE");
        // stringList.remove("java");
        // stringList.set(2, "java EE");
        System.out.println(stringList);
    
        // set集合不允许有重复元素
        Set<String> set = Set.of("Hello", "World", "java");
        // UnsupportedOperationException
        // set.add("java EE");
        // set.remove("java");
        System.out.println(set);
    }
    

    注释掉的部分是不支持的内容,不允许的部分。

    五、集合

    提供一种存储空间可变的存储模型,存储的数据容量可以随时发生改变

    集合分为单列集合(Collection:单值形式)和双列集合(Map:K-V形式,键值对)

    集合(黑体的是接口,其余的为实现类)

    • Collection 单列集合
      • List 元素可重复
        • ArrayList
        • LinkedList
      • Set 元素不可重复
        • HashSet
        • TreeSet
    • Map 双列集合
      • HashMap

    Collection

    Collection集合概述

    • 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素
    • JDK不提供此接口的任何直接实现,它提供更具体的子接口(如Set和List) 实现

    创建Collection集合的对象

    • 多态的方式
    • 具体的实现类,如ArrayList
    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<String>();
        collection.add("Hello");
        collection.add("World");
        collection.add("java");
        System.out.println(collection);
    }
    

    输出的结果是:[Hello, World, java]

    说明ArrayList实现类中重写了toString方法

    常用方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMHSvdQq-1639662744842)(https://i.loli.net/2021/04/25/BzK4NoIw5sya7bL.png)]

    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<String>();
        // 添加对应元素
        collection.add("Hello");
        // 移除指定元素
        collection.remove("Hello");
        collection.add("World");
        // 清除所有元素
        collection.clear();
        collection.add("java EE");
        // 是否包含特定元素
        System.out.println(collection.contains("java EE"));
        // 判断集合是否为空
        System.out.println(collection.isEmpty());
        // 返回集合的元素个数
        System.out.println(collection.size());
        System.out.println(collection);
    }
    

    集合中元素的遍历

    利用迭代器Iterator,集合的专用遍历方式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0UofVd4k-1639662744842)(https://i.loli.net/2021/04/25/4DdeiuUkxHIrbJa.png)]

    public static void main(String[] args) {
        Collection<String> collection = new ArrayList<String>();
        collection.add("Hello");
        collection.add("World");
        collection.add("java");
        System.out.println(collection);
    
        // 获得迭代器的方法
        Iterator<String> iterator = collection.iterator();
        // 获取元素的方法
        System.out.println(iterator.next());
        /*System.out.println(iterator.next());
        System.out.println(iterator.next());
        System.out.println(iterator.next());
        System.out.println(iterator.next());*/
        // 正确的遍历方法,使用 hasNext 方法判断是否有下一个元素再进行访问
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
    

    集合的使用步骤

    image-20210425014246288

    List

    image-20210425014446543

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("World");
        list.add("java");
        list.add("World");
        System.out.println(list);
        // 采用迭代器的方式进行遍历列表
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        // 采用for循环的方式遍历列表
        for (String s : list) {
            System.out.println(s);
        }
    }
    

    可重复,遍历方式有两种

    特有方法

    image-20210425015116983

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("World");
        // 在集合中的指定位置插入指定的元素
        list.add(2, "java");
        // 在集合中删除指定索引处的元素
        list.remove(2);
        // 修改指定位置处的元素
        System.out.println(list.set(1, "java EE"));
        // 返回指定位置的元素
        System.out.println(list.get(1));
        System.out.println(list);
    }
    

    并发修改异常

    需求:在集合中遍历元素,如果发现有World元素存在,我们就往集合中添加新的元素

    public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            list.add("Hello");
            list.add("World");
            list.add("java");
            // ConcurrentModificationException : 并发修改异常
            /*Iterator<String> iterator = list.iterator();
            while(iterator.hasNext()) {
                String s = iterator.next();
                if ("World".equals(s)) {
                    list.add("java EE");
                }
            }*/
            // ConcurrentModificationException
            /*for (String s : list) {
                if ("World".equals(s)) {
                    System.out.println(list.add("java EE"));;
                }
            }*/
    
            for (int i = 0; i < list.size(); i++) {
                String s = list.get(i);
                if ("World".equals(s)) {
                    System.out.println(list.add("java EE"));;
                }
            }
    
            System.out.println(list);
        }
    

    注释部分运行结果:

    Exception in thread “main” java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 1012 ) a t j a v a . b a s e / j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:1012) at java.base/java.util.ArrayList Itr.checkForComodification(ArrayList.java:1012)atjava.base/java.util.ArrayListItr.next(ArrayList.java:966)
    at itheima_04.ListDemo01.main(ListDemo01.java:16)

    这时候会报错,是因为并发修改异常。在源代码中查看,我们可以看到在遍历集合的时候,会出现两个参数

    • modCount 实际修改次数
    • expectedModCount 预期修改次数

    当这两个值不相等的时候,会抛出并发修改异常。add方法运行时候对modCount做了+1操作,但是这时候expectedModCount并没有执行+1操作。下次访问的时候,Itr类中存在的判断两者相等操作执行,得出两者的不相等,抛出异常。

    利用ArrayList修改元素,使用迭代器进行遍历的时候,会使得预期修改次数得不到该有的变化。

    迭代的时候,之所以设置两个参数,不允许添加元素,是因为如果一直在迭代的时候添加元素,可能会造成迭代永远不会结束的情况。

    可以看出,foreach循环(增强for循环)在这里本质上也是使用了迭代器运行。(查看语法糖可以得知,底层也是利用了迭代器进行实现的)

    用for循环实现的时候,则没有出现异常情况,可以正常运行。

    true
    [Hello, World, java, java EE]

    • 并发修改异常:ConcurrentModificationException
    • 产生原因:
      • 迭代器遍历的过程中,通过集合对象修改了集合中元素的长度,造成了迭代器获取元素中判断预期修改值和实际修改值不一致
    • 解决方案
      • 用for循环遍历,然后用集合对象做对应的操作即可

    迭代器的本质理解:

    迭代器的作用是将集合中的元素按照顺序遍历访问,依次返回第0号元素、第1号元素等。如果这个时候访问到第3号元素的时候,往第0号元素的位置添加元素,那么后面的所有元素都将往后移位置,也就是第3号元素会出现重复访问,造成严重后果。

    Java认为,在迭代的时候,容器大小应该保持不变。这也是为什么会在迭代器中设置两个变量modCount以及expectedModCount是否相等的判断,用这两个元素进行比较,如果发生添加或删除操作,那么modCount就会+1,而期望值却没有发生改变,导致两者数值不一致,从而抛出异常。

    之所以利用普通for循环调用get方法能够实现,是因为在get方法中并没有添加二者的判断。modCount只存在于add或者remove方法中,并不存在get方法中,也没有必要再get方法中添加二者的数值相等的判断。

    ListIterator

    列表迭代器:通过list集合的listiterator方法得到,是list集合的特有迭代器。

    相比父类Iterator,它可以随意设置遍历的顺序,并且能够在迭代的时候进行修改。

    向后遍历 hsaNext

    向前遍历 hasPrevious

    image-20210425150053550

    这里面之所以可以遍历时进行修改,是因为在源码中,将modCount赋值给了expectedModCount了,不会造成二者的不相等。

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("World");
        list.add("java");
    
        // 获取list迭代器
        ListIterator<String> stringListIterator = list.listIterator();
        // 向后遍历
        while (stringListIterator.hasNext()) {
            String s = stringListIterator.next();
            if ("World".equals(s)) {
                stringListIterator.add("java EE");
            }
        }
        System.out.println(list);
        // 向前遍历
        while (stringListIterator.hasPrevious()) {
            System.out.println(stringListIterator.previous());
        }
    }
    

    增强for循环(for each)

    增强for:简化数组和Collection集合的遍历

    • 实现Iterable接口的类允许其对象成为增强型 for语句的目标
    • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器

    增强for的格式

    • 格式:

      for(元素数据类型变量名 : 数组或者Collection集合) {
      	//在此处使用变量即可,该变量就是元素
      }
      

    编写代码如下所示:

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
    
        for (int i : arr) {
            System.out.println(i);
        }
    
        String[] strings = {"Hello", "World", "java"};
        for (String string : strings) {
            System.out.println(string);
        }
    
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("World");
        list.add("java");
        // foreach内部是一个Iterator迭代器
        for (String s : list) {
            System.out.println(s);
            if (s.equals("World")) {
                list.add("java EE");
            }
        }
    }
    

    运行结果如下所示,在Iterator迭代器中,修改集合的数量操作,报出并发修改异常

    image-20210427014507547

    List常用子类

    ArrayList:底层数据结构是数组,查询快,增删慢

    LinkedList:底层数据结构是链表,查询慢,增删快

    // 用 ArrayList 和 LinkedList 完成存储字符串并遍历
    public static void main(String[] args) {
        // 创建集合对象
        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList.add("Hello");
        arrayList.add("World");
        arrayList.add("java");
        for (String s : arrayList) {
            System.out.println(s);
        }
        Iterator<String> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        for (int i = 0; i < arrayList.size(); i++) {
            System.out.println(arrayList.get(i));
        }
    
        LinkedList<String> linkedList = new LinkedList<String>();
        linkedList.add("Hello");
        linkedList.add("World");
        linkedList.add("java EE");
        for (String s : linkedList) {
            System.out.println(s);
        }
        Iterator<String> stringIterator = linkedList.iterator();
        while (stringIterator.hasNext()) {
            System.out.println(stringIterator.next());
        }
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }
    }
    

    LinkedList集合的特有功能

    image-20210427212018137

    public static void main(String[] args) {
        // 测试 LinkedList 集合特有功能
        LinkedList<String> list = new LinkedList<String>();
        list.add("Hello");
        list.add("World");
        list.add("java");
    
        // 经测试,LinkedList 集合重写了toString方法
        System.out.println(list.toString());
        list.addFirst("First");
        list.addLast("Last");
        System.out.println(list.toString());
        list.removeFirst();
        list.removeLast();
        System.out.println(list.toString());
        System.out.println(list.getFirst());
        System.out.println(list.getLast());
    }
    

    Set

    ①不包含重复元素的集合

    ②没有带索引的方法,所以不能用普通for循环进行遍历

    /*
    * HashSet : 对集合的迭代顺序不做任何保证
    * 无须且不重复,不能添加重复的元素(添加之后无效,不报错)
    * */
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        set.add("Hello");
        set.add("World");
        set.add("java");
        // set.add("java");
        System.out.println(set.toString());
        for (String s : set) {
            System.out.println(s);
        }
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
    

    哈希值

    哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值

    Object类中有一个方法可以获取对象的hash值

    • 同一个对象多次调用hashCode,获得的hash值是相同的
    • 通过重写hashCode方法,可以让不同对象获得的hashCode值相同
    • 但是在特殊情况下,有可能不同的对象在不重写方法的情况下还是会出现相同的hashCode值
    public static void main(String[] args) {
        Student student = new Student("张三", 20);
        // 同一个对象多次调用HashCode方法,输出的hash值是一样的
        System.out.println(student.hashCode()); // 189568618
        System.out.println(student.hashCode()); // 189568618
        Student student2 = new Student("张三", 20);
        System.out.println(student2.hashCode()); // 793589513
        System.out.println(student2.hashCode()); // 793589513
        // 通过在类中重写hashCode方法,可以实现不同对象返回相同的hash值
        System.out.println("Hello".hashCode()); // 69609650
        System.out.println("World".hashCode()); // 83766130
        System.out.println("重地".hashCode()); // 1179395
        System.out.println("通话".hashCode()); // 1179395
    }
    

    HashSet保证元素唯一性分析

    image-20210427221941612

    HashSet<String> hashSet = new HashSet<String>();
    hashSet.add("Hello");
    hashSet.add("World");
    hashSet.add("java");
    System.out.println(hashSet);
    
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    // hash值是根据元素的hashCode()方法得到的
    // hash值和元素的hashCode方法相关
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果哈希表未初始化,就对哈希表进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据对象的hash值计算对象的存储位置,如果该位置没有元素,则存储元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /*
            1. 将存入的元素和以前的元素比较hash值
               如果hash值不同,则表示存入的元素为新元素(HashSet中没有的元素)
               会继续向下执行,将元素添加进hashSet中
            2. 如果hash值相同,则会调用对象的equals方法进行比较
                   如果返回false,会继续向下执行,把元素添加到集合
                   如果返回true,说明元素重复,不存储
            */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    哈希表

    哈希表是一种特殊的数据结构,通过链表+数组的方式实现

    在jdk 8之后,对哈希表底层做了优化

    image-20210427222307522

    关于存储对象中,使用哈希表存储不同的对象:

    /*
    * 要求用 HashSet存储集合,并且保证集合中元素的唯一性
    * */
    public static void main(String[] args) {
        // 创建 HashSet 的 Student 集合对象
        HashSet<Student> hashSet = new HashSet<Student>();
        // 创建 Student 对象
        Student student1 = new Student("张三", 18);
        Student student2 = new Student("李四", 19);
        Student student3 = new Student("王五", 20);
        Student student4 = new Student("王五", 20);
        // 把学生对象添加到 HashSet 中
        hashSet.add(student1);
        hashSet.add(student2);
        hashSet.add(student3);
        hashSet.add(student4);
        for (Student student : hashSet) {
            System.out.println(student.toString());
        }
    }
    

    这样的情况下,直接使用hashSet会使得student3和student4对象同时均添加进集合中

    为了解决这样的情况,我们在Student类中重写equal()和hashCode()方法

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(stuName, student.stuName);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(stuName, age);
    }
    

    LinkedHashSet集合

    image-20210427223542289

    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>();
        linkedHashSet.add("Hello");
        linkedHashSet.add("World");
        linkedHashSet.add("World");
        linkedHashSet.add("java");
        for (String s : linkedHashSet) {
            System.out.println(s);
        }
    }
    

    添加元素的时候,不会出现重复的元素,由哈希表保证元素的唯一性,由链表保证元素的有序

    TreeSet集合

    image-20210427224129129

    public static void main(String[] args) {
        TreeSet<Integer> treeSet = new TreeSet<Integer>();
        treeSet.add(10);
        treeSet.add(50);
        treeSet.add(30);
        treeSet.add(40);
        treeSet.add(20);
        treeSet.add(30);
        for (Integer integer : treeSet) {
            System.out.println(integer);
        }
    }
    

    输出的结果是10 20 30 40 50其中不包含重复元素,而且按照自然排序进行输出

    自然排序Comparable的使用

    将学生存储金TreeSet集合中,使用无参构造对学生对象根据年龄进行排序

    年龄相同时,按照字母顺序进行排序

    /*
    * 将学生按照年龄排序,如果年龄一样,按照字母顺序排序
    * */
    public static void main(String[] args) {
        TreeSet<Student> treeSet = new TreeSet<Student>();
        Student student1 = new Student("张三", 17);
        Student student2 = new Student("李四", 20);
        Student student3 = new Student("王五", 18);
        Student student4 = new Student("赵六", 18);
        treeSet.add(student1);
        treeSet.add(student2);
        treeSet.add(student3);
        treeSet.add(student4);
        for (Student student : treeSet) {
            System.out.println(student);
        }
    }
    

    在Student类中,重写compareTo方法,并且实现Comparable接口

    public class Student implements Comparable<Student>{
        @Override
        public int compareTo(Student o) {
            /*return 0; // 认为元素是重复的
            return 1; // 将元素按照正序输出
            return -1; // 将元素按照反序输出*/
            // 按照年龄从小到大排序
            int i = this.age - o.age; // 按照升序排列
            // int i = o.age - this.age; // 按照降序排列
            // 按照字母排序(年龄一样的情况下)
            int i1 = i == 0 ? this.stuName.compareTo(o.stuName) : i;
            return i1;
        }
    }
    

    结论:

    • 用TreeSet存储自定义对象集合的时候,无参构造使用的是自然排序对元素进行排序的
    • 自然排序,就是让元素所属的类实现Comparable接口,并且重写compareTo(Object o)方法
    • 重写方法时,一定要按照规定的要求的主要条件和次要条件来编写

    比较器Comparator的使用

    /*
     * 将学生按照年龄排序,如果年龄一样,按照字母顺序排序
     * */
    public static void main(String[] args) {
        TreeSet<Student> treeSet = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                int num = s1.getAge() - s2.getAge();
                int num2 = num == 0 ? s1.getStuName().compareTo(s2.getStuName()) : num;
                return num2;
            }
        });
        Student student1 = new Student("张三", 17);
        Student student2 = new Student("李四", 20);
        Student student3 = new Student("王五", 18);
        Student student4 = new Student("赵六", 18);
        treeSet.add(student1);
        treeSet.add(student2);
        treeSet.add(student3);
        treeSet.add(student4);
        for (Student student : treeSet) {
            System.out.println(student);
        }
    }
    

    结论:

    • 用TreeSet存储自定义对象集合的时候,带参方法使用的是比较器排序对元素进行排序的
    • 自然排序,就是让集合构造方法接收Comparator的实现类,并且重写compareTo(Object o)方法
    • 重写方法时,一定要按照规定的要求的主要条件和次要条件来编写

    案例:用TreeSet集合类存储多个学生信息,并且按照总分成绩进行排序(语文成绩+数学成绩)

    ①创建Student类

    public class Student {
    
        private String name;
        private int chinese;
        private int math;
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", chinese=" + chinese +
                    ", math=" + math + ", total=" +
                    (math + chinese) + '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return chinese == student.chinese && math == student.math && Objects.equals(name, student.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, chinese, math);
        }
    
        public Student(String name, int chinese, int math) {
            this.name = name;
            this.chinese = chinese;
            this.math = math;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getChinese() {
            return chinese;
        }
    
        public void setChinese(int chinese) {
            this.chinese = chinese;
        }
    
        public int getMath() {
            return math;
        }
    
        public void setMath(int math) {
            this.math = math;
        }
    }
    

    ②编写主程序

    public static void main(String[] args) {
        TreeSet<Student> students = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                int num = s1.getChinese() + s1.getMath() - s2.getChinese() - s2.getMath();
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                return -num2;
            }
        });
        Student student1 = new Student("张三", 60, 80);
        Student student2 = new Student("李四", 70, 60);
        Student student3 = new Student("王五", 85, 75);
        Student student4 = new Student("赵六", 90, 65);
        students.add(student1);
        students.add(student2);
        students.add(student3);
        students.add(student4);
        for (Student student : students) {
            System.out.println(student);
        }
    }
    

    输出结果实现了根据总分降序排列的需求,并且成绩相同的情况下根据姓名进行了排序。

    Student{name=‘王五’, chinese=85, math=75, total=160}
    Student{name=‘赵六’, chinese=90, math=65, total=155}
    Student{name=‘张三’, chinese=60, math=80, total=140}
    Student{name=‘李四’, chinese=70, math=60, total=130}

    Map

    interface Map<K,V>

    将键映射到值的对象,不能包含重复的键,每个键可以映射到最多一个值

    举例:学生姓名和学号的关系,学号就是键,值就是姓名

    创建Map采用多态的方式,我们选用的是hashMap

    public static void main(String[] args) {
        Map<String, String> map = new java.util.HashMap<String, String>();
        map.put("18408000101", "张三");
        map.put("18408000102", "李四");
        map.put("18408000103", "王五");
        // 键重复的时候,会使用新添加的值覆盖掉之前的值
        map.put("18408000103", "赵六");
        System.out.println(map);
        // {18408000101=张三, 18408000102=李四, 18408000103=赵六}
    }
    

    Map集合的基本功能

    image-20210428104144495

    public static void main(String[] args) {
        // 创建集合元素
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1, "张三");
        map.put(2, "李四");
        map.put(3, "王五");
        map.put(4, "王五");
        // 返回的是键所对应的值
        System.out.println(map.remove(1));
        System.out.println(map);
        // 移除所有键值对数据
        /*map.clear();
        System.out.println(map);*/
        // 是否包含键
        System.out.println(map.containsKey(2));
        // 是否包含数据
        System.out.println(map.containsValue("王五"));
        // 是否为空
        System.out.println(map.isEmpty());
        // 输出长度
        System.out.println(map.size());
    }
    

    Map集合的获取功能

    image-20210428105302542

    public static void main(String[] args) {
        // 创建集合元素
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1, "张三");
        map.put(2, "李四");
        map.put(3, "王五");
        // 根据键返回值
        System.out.println(map.get(1));;
        // 返回所有键的集合
        Set<Integer> integers = map.keySet();
        System.out.println(integers);
        // 返回所有值的集合
        Collection<String> values = map.values();
        System.out.println(values);
    }
    

    Map集合的遍历

    public static void main(String[] args) {
        // 创建集合元素
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1, "张三");
        map.put(2, "李四");
        map.put(3, "王五");
    
        // 1. 获取所有键的集合
        Set<Integer> integers = map.keySet();
        for (Integer integer : integers) {
            System.out.println(integer + "," + map.get(integer));
        }
        // 2. 利用entrySet获取对象集合
        Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
        for (Map.Entry<Integer, String> entry : entrySet) {
            System.out.println(entry.getKey() + "," + entry.getValue());
        }
    }
    

    遍历Map有两种方式:获取所有键的集合,根据键找到对应的值;利用entrySet获取到Map中的每一对元素,之后调用getKey和getValue方法得到键值对的值

    HashMap存储学生对象并遍历

    public static void main(String[] args) {
        Map<String, Student> map = new HashMap<String, Student>();
        Student student1 = new Student("张三", 18);
        Student student2 = new Student("李四", 19);
        Student student3 = new Student("王五", 20);
        map.put("001", student1);
        map.put("002", student2);
        map.put("003", student3);
        Set<Map.Entry<String, Student>> entries = map.entrySet();
        for (Map.Entry<String, Student> entry : entries) {
            System.out.println(entry.getKey() + entry.getValue().getName() + entry.getValue().getAge());
        }
        Set<String> set = map.keySet();
        for (String s : set) {
            System.out.println(s + map.get(s).getName() + map.get(s).getAge());
        }
    }
    

    这个项目中,想要保留键的唯一性的案例,就需要在键的类(Student)中重写equals方法和hashCode方法

    ArrayList集合存储HashMap集合元素并遍历

    ①创建ArrayList集合

    ②创建HashMap集合,并添加键值对元素

    ③把HashMap作为元素添加到ArrayList集合

    ④遍历ArrayList集合

    public static void main(String[] args) {
        ArrayList<HashMap<String, String>> arrayList = new ArrayList<HashMap<String, String>>();
        HashMap<String, String> hashMap1 = new HashMap<String, String>();
        hashMap1.put("周瑜", "小乔");
        HashMap<String, String> hashMap2 = new HashMap<String, String>();
        hashMap2.put("孙策", "大乔");
        arrayList.add(hashMap1);
        arrayList.add(hashMap2);
        System.out.println(arrayList);
        for (HashMap<String, String> hashMap : arrayList) {
            Set<String> set = hashMap.keySet();
            for (String key : set) {
                System.out.println(key + "," + hashMap.get(key));
            }
        }
    }
    

    HashMap集合存储ArrayList元素并遍历

    ①创建HashMap集合

    ②创建ArrayList集合,添加元素

    ③把ArrayList元素作为元素添加进HashMap中

    public static void main(String[] args) {
        HashMap<String, ArrayList<String>> hashMap = new HashMap<String, ArrayList<String>>();
        ArrayList<String> arrayList1 = new ArrayList<String>();
        arrayList1.add("诸葛亮");
        arrayList1.add("赵云");
        ArrayList<String> arrayList2 = new ArrayList<String>();
        arrayList2.add("贾宝玉");
        arrayList2.add("林黛玉");
        hashMap.put("三国演义", arrayList1);
        hashMap.put("红楼梦", arrayList2);
        Set<String> set = hashMap.keySet();
        for (String key : set) {
            System.out.println("《" + key + "》");
            ArrayList<String> arrayList = hashMap.get(key);
            for (String s : arrayList) {
                System.out.println(s);
            }
            System.out.println("《" + key + "》" + ": " + arrayList);
        }
    }
    

    统计字符串中每个字符出现的次数

    要求编写一个程序,接收输入的字符串,统计字符串出现的次数并按照要求的格式输出

    请输入字符:
    cccbbbaaaddd
    a(3)b(3)c(3)d(3)

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入字符:");
        String line = scanner.nextLine();
        // 创建HashMap存储结果
        // 如果想要排序的结果,使用treeMap即可
        // HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>();
        TreeMap<Character, Integer> hashMap = new TreeMap<Character, Integer>();
        // 遍历每一个字符,得到各字符对应的数字
        for (int i = 0; i < line.length() ; i++) {
            char key = line.charAt(i);
            // 去 hashMap 中寻找看看是否存在,不存在就添加,存在就+1
            Integer value = hashMap.get(key);
    
            if (value == null) {
                hashMap.put(key, 1);
            } else {
                value++; // 此处进行了拆箱操作,需要再进行装箱操作,才能传进去
                hashMap.put(key, value);
            }
        }
    
        // 遍历HashMap集合,按要求输出结果
        StringBuilder stringBuilder = new StringBuilder();
    
        Set<Character> characters = hashMap.keySet();
        for (Character key : characters) {
            stringBuilder.append(key).append("(").append(hashMap.get(key)).append(")");
        }
        System.out.println(stringBuilder);
    }
    

    Collections

    image-20210429191917166

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<Integer>();
        arrayList.add(40);
        arrayList.add(20);
        arrayList.add(10);
        arrayList.add(50);
        arrayList.add(30);
        // 将list中的元素反转顺序输出
        Collections.reverse(arrayList);
        System.out.println(arrayList);
        // 将list中的元素排序输出
        Collections.sort(arrayList);
        System.out.println(arrayList);
        // 将list中的元素按照随机顺序排序
        Collections.shuffle(arrayList);
        System.out.println(arrayList);
    }
    

    输出结果中,第三行的结果每一次都不一样

    [30, 50, 10, 20, 40]
    [10, 20, 30, 40, 50]
    [50, 30, 40, 20, 10]

    ArrayList集合存储学生对象并排序

    使用ArrayList存储学生对象并排序,利用Collections对学生进行排序,并遍历ArrayList

    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<Student>();
        Student student1 = new Student("zhangsan", 20);
        Student student2 = new Student("lisi", 19);
        Student student3 = new Student("wangwu", 18);
        Student student4 = new Student("wangw", 18);
    
        students.add(student1);
        students.add(student2);
        students.add(student3);
        students.add(student4);
    
        // 第一种方法,在Student内部实现Comparable
        // Collections.sort(students);
        // 第二种方法,使用匿名内部类
        Collections.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                int num = s1.getAge() - s2.getAge();
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                return num2;
            }
        });
    
        System.out.println(students);
    
        for (Student student : students) {
            System.out.println(student);
        }
    }
    

    案例:模拟斗地主中的洗牌、发牌、看牌

    ①创建一个牌盒,也就是创建一个ArrayList集合对象

    ②往牌盒里面装牌,添加元素

    ③洗牌,把牌的顺序打乱,用shuffle方法实现

    ④发牌,遍历集合,给三个玩家发牌

    ⑤看牌,三个玩家分别遍历自己的牌

    public class PokerSimulation {
        /*
        * 模拟斗地主中的洗牌、发牌和看牌
        * ①创建一个牌盒,也就是创建一个ArrayList集合对象
        * ②往牌盒里面装牌,添加元素
        * ③洗牌,把牌的顺序打乱,用shuffle方法实现
        * ④发牌,遍历集合,给三个玩家发牌
        * ⑤看牌,三个玩家分别遍历自己的牌
        */
        public static void main(String[] args) {
            // ①创建一个牌盒,也就是创建一个ArrayList集合对象
            ArrayList<String> array = new ArrayList<String>();
            // ②往牌盒里面装牌,添加元素
            /*
            * Joker1、Joker2
            * ♦2、♦3、、、♦K、♦A
            * ♣2、♣3、、、
            * ♠2、♠3、、、
            * ♥2、♥3、、、
            * */
            // 定义花色数组
            String[] colors = {"♠", "♥", "♣", "♦"};
            // 定义点数数组
            String[] numbers = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
            // 添加进ArrayList
            for (String color : colors) {
                for (String number : numbers) {
                    array.add(color + number);
                }
            }
            array.add("JokerSmall");
            array.add("JokerBig");
            // ③洗牌,把牌的顺序打乱,用shuffle方法实现
            Collections.shuffle(array);
            // ④发牌,遍历集合,给三个玩家发牌
            ArrayList<String> user1 = new ArrayList<String>();
            ArrayList<String> user2 = new ArrayList<String>();
            ArrayList<String> user3 = new ArrayList<String>();
            ArrayList<String> landlord = new ArrayList<String>();
            for (int i = 0; i < array.size(); i++) {
                String poke = array.get(i);
                if (i >= array.size() - 3) {
                    landlord.add(poke);
                } else if (i % 3 == 0) {
                    user1.add(poke);
                } else if (i % 3 == 1) {
                    user2.add(poke);
                } else if (i % 3 == 2) {
                    user3.add(poke);
                }
            }
            // ⑤看牌,三个玩家分别遍历自己的牌
            lookPoke("张三", user1);
            lookPoke("李四", user2);
            lookPoke("王五", user3);
            lookPoke("底牌", landlord);
    
            System.out.println(array);
        }
    
        private static void lookPoke (String name, ArrayList<String> arrayList) {
            System.out.print(name + "的牌是: ");
            for (String poke : arrayList) {
                System.out.print(poke + " ");
            }
            System.out.println();
        }
    
    }
    

    整体运行结果如下所示:

    张三的牌是: ♥7 ♥Q ♠3 ♦6 ♦J ♥3 ♣5 ♥9 ♥4 ♠J ♠10 ♠4 ♥8 ♦K ♦A ♣9 ♠K
    李四的牌是: ♣2 ♥K ♦9 ♦3 ♣J ♥10 ♣4 ♣8 ♠6 ♠9 ♠A ♠7 ♠8 ♦7 ♥5 ♣7 ♣K
    王五的牌是: ♠2 ♥A ♦10 ♦Q ♦4 ♣3 ♠Q JokerSmall ♠5 ♥2 ♣Q ♦2 ♣6 ♦5 ♦8 ♣10 ♣A
    底牌的牌是: ♥J ♥6 JokerBig

    案例:将斗地主中的牌进行排序

    image-20210429203158066

    ①创建HashMao集合,键是编号,值是牌

    ②创建ArrayLIst存储编号

    ③将三个玩家的牌的编号存进TreeSet中

    ④将TreeSet中的编号取出来,从HashMap中获得对应的牌

    ⑤洗牌,将编号打乱,用Collections中的shuffle方法打乱

    ⑥发牌,发的也是编号,将编号用TreeSet存储,会直接输出有序序列

    ⑦看牌,定义看牌方法,根据编号从HashMap中获取到牌

    ⑧调用看牌方法

    实现:

    public class PokeDemo {
        public static void main(String[] args) {
            // ①创建HashMao集合,键是编号,值是牌
            HashMap<Integer, String> hashMap = new HashMap<Integer, String>();
            // ②创建ArrayList存储编号
            ArrayList<Integer> array = new ArrayList<Integer>();
            // 定义花色数组
            String[] colors = {"♦", "♣", "♥", "♠"};
            // 定义点数数组
            String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
            // 定义编号,从0开始往HashMap中存储数据
            int index = 0;
            for (String number : numbers) {
                for (String color : colors) {
                    hashMap.put(index, color + number);
                    array.add(index);
                    index++;
                }
            }
            hashMap.put(index, "JokerSmall");
            array.add(index);
            index++;
            hashMap.put(index, "JokerBig");
            array.add(index);
            // ③洗牌,将编号打乱,用Collections中的shuffle方法打乱
            Collections.shuffle(array);
            // ④发牌,发的也是编号,将编号用TreeSet存储,会直接输出有序序列
            TreeSet<Integer> user1 = new TreeSet<Integer>();
            TreeSet<Integer> user2 = new TreeSet<Integer>();
            TreeSet<Integer> user3 = new TreeSet<Integer>();
            TreeSet<Integer> landlord = new TreeSet<Integer>();
    
            for (int i = 0; i < array.size(); i++) {
                if (i >= array.size() - 3) {
                    landlord.add(array.get(i));
                } else if (i % 3 == 0) {
                    user1.add(array.get(i));
                } else if (i % 3 == 1) {
                    user2.add(array.get(i));
                } else if (i % 3 == 2) {
                    user3.add(array.get(i));
                }
            }
    
            // ⑥调用看牌方法
            lookPoke("张三", user1, hashMap);
            lookPoke("李四", user2, hashMap);
            lookPoke("王五", user3, hashMap);
            lookPoke("底牌", landlord, hashMap);
        }
    
        // ⑤看牌,定义看牌方法,根据编号从HashMap中获取到牌
        private static void lookPoke(String name, TreeSet<Integer> treeSet, HashMap<Integer, String> hashMap) {
            System.out.print(name + "的牌是: ");
            for (Integer key : treeSet) {
                String value = hashMap.get(key);
                System.out.print(value + " ");
            }
            System.out.println();
        }
    }
    

    六、IO流

    File

    是文件和目录的抽象表示

    • 文件和目录是可以通过File封装成对象的
    • 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。它可以是存在的,也可以是不存在的。将来是要通过具体的操作把这个路径的内容转换为具体存在的

    image-20210429213119090

    public static void main(String[] args) {
        File file1 = new File("D:\\Java\\java.txt");
        System.out.println(file1);
        File file2 = new File("D:\\Java", "java.txt");
        System.out.println(file2);
        File file3 = new File("D:\\Java");
        File file4 = new File(file3, "java.txt");
        System.out.println(file4);
    }
    

    输出内容:

    D:\Java\java.txt
    D:\Java\java.txt
    D:\Java\java.txt

    磁盘中并没有添加新的文件,路径下没有新建的java.txt文件

    File类创建功能

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4eAlKFi-1639662744847)(https://i.loli.net/2021/04/29/mzpvDVAc59SNuB6.png)]

    public static void main(String[] args) throws IOException {
        File file1 = new File("D:\\Java\\java.txt");
        // boolean createNewFile 创建新文件,成功返回true,否则false
        System.out.println(file1.createNewFile());
    
        File file2 = new File("D:\\Java\\Test");
        // boolean mkdir 创建对应的目录,成功返回true,否则false
        // 创建由此命名的抽象目录
        System.out.println(file2.mkdir());
    
        File file3 = new File("D:Java\\JTest\\HTML");
        // boolean mkdirs 创建对应的抽象目录,成功返回true,否则false
        // 创建由此命名的抽象目录,包括必需但不存在的父目录
        System.out.println(file3.mkdirs());
    }
    

    File类判断和获取功能

    image-20210429215744326

    public static void main(String[] args) throws IOException {
        File file = new File("D:\\Java\\java.txt");
        file.createNewFile();
        System.out.println(file.isDirectory());
        System.out.println(file.isFile());
        System.out.println(file.exists());
    
        System.out.println(file.getAbsolutePath()); // 返回文件的绝对路径
        System.out.println(file.getPath()); // 将此抽象路径名转换成字符串
        System.out.println(file.getName()); // 返回文件的名称
        System.out.println("-------------");
    
        File file1 = new File("D:\\Java");
        String[] list = file1.list(); // 返回的是抽象目录下的文件以及文件目录对应的名称字符串数组
        for (String str : list) {
            System.out.println(str);
        }
        System.out.println("-------------");
        File[] files = file1.listFiles(); // 返回此抽象目录中的文件以及文件目录的File对象
        for (File f : files) {
            System.out.println(f);
        }
    }
    

    输出结果:

    false
    true
    true
    D:\Java\java.txt
    D:\Java\java.txt

    java.txt

    apache-tomcat-8.5.65
    apache-tomcat-8.5.65-windows-x64.zip
    D:\Java\apache-tomcat-8.5.65
    D:\Java\apache-tomcat-8.5.65-windows-x64.zip

    Process finished with exit code 0

    File类删除功能

    image-20210430153149746

    public static void main(String[] args) throws IOException {
        File file = new File("D:\\Java\\java.txt");
        System.out.println(file.createNewFile());
        boolean delete = file.delete();   // boolean delete()返回的是布尔值,删除操作成功返回true
        System.out.println(delete);
        File file1 = new File("D:\\Java\\javatest");
        System.out.println(file1.mkdir());
        System.out.println(file1.delete());
    }
    

    递归

    递归,指的是程序方法调用方法本身

    解决的问题:

    把一个复杂的问题层层转化为一个个与原问题相似的较小的问题

    递归策略只需要少量的程序就可以描述出解题过程中所需要的多次重复计算

    /*打印斐波那契数列*/
    public static void main(String[] args) {
        System.out.println(printNumber(20));
    }
    
    private static int printNumber(int n) {
        if (n == 1 || n == 2) {
            return 1;
        } else {
            return printNumber(n - 1) + printNumber(n - 2);
        }
    }
    

    案例:递归求阶乘

    public static void main(String[] args) {
        System.out.println(factorial(5));
    }
    private static int factorial(int n) {
        if (n == 1) {
            return 1;
        } else {
            return factorial(n - 1) * n;
        }
    }
    

    运行结果:正常打印120

    运行时的内存图:进栈过程

    image-20210430154848124

    运行时的内存图:出栈过程

    image-20210430154933302

    案例:遍历目录

    给定一个指定的目录路径,遍历该目录下的所有内容,并将文件的绝对路径名打印出来

    public static void main(String[] args) {
        // ①根据给定的路径创建一个file对象
        File srcFile = new File("D:\\Java");
        // ⑥调用方法
        getFilePath(srcFile);
    }
    // ②定义一个方法,用于获取给定目录下的所有内容
    private static void getFilePath(File srcFile) {
        // ③获取给定的File目录下的所有文件或者目录的File[]数组
        File[] files = srcFile.listFiles();
        // ④遍历该File数组,得到每一个对象
        if (files != null) {
            for (File file : files) {
                // ⑤判断该File对象是否是目录
                if (file.isDirectory()) {
                    getFilePath(file); // 是目录,递归调用
                } else if (file.isFile()) {
                    System.out.println(file.getAbsolutePath()); // 是文件,直接打印输出
                }
            }
        }
    }
    

    输出的是所有文件的绝对路径名以及文件名称、后缀名

    字节流

    • IO:input/output,输入输出
    • 流:是一种抽象概念,是数据传输的总称,也就是说数据在设备之间的传输称为流,流的本质是数据传输
    • IO流就是用来处理设备间的数据传输问题
      • 常见的应用:文件复制、文件上传、文件下载

    分类

    • 按照数据的流向
      • 输入流:读数据
      • 输出流:写数据
    • 按照数据类型
      • 字节流
        • 字节输入流、字节输出流
      • 字符流
        • 字符输入流、字符输出流

    IO流的分类默认是按照数据类型来分类的。默认情况下,如果用记事本打开文件之后能读懂的内容,就使用字符流,否则字节流。

    如果在不知道什么文件的情况下,使用字节流。

    字节流写数据

    • InputStream:这个抽象类是表示字节输入流的所有类的超类

    • OutputStream:这个抽象类是表示字节输出流的所有类的超类

    • 子类名特点:子类名称都是以其父类名称作为子类名的后缀

    FileOutputStream:文件输出流用于将数据写入File

    • FileOutputStream(String name):创建文件输出流以指定的名称写入文件
    • FileOutputStream(File file):创建文件输出流以写入由指定的File对象表示的文件
    • FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件,第二个参数为true,则从文件末尾写入数据而不是文件开头

    使用字节输出流写数据的步骤:

    • 创建字节输出流对象:调用系统功能创建了文件,创建字节输出流对象,让字节输出流对象指向文件
    • 调用字节输出流的写数据方法
    • 释放资源:关闭字节输出流对象以及所有和字节输出流相关的系统资源

    字节流写数据的三种方式

    image-20210430180513479

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("D:\\Java\\java.txt");
        /*File file = new File("D:\\Java\\java.txt");
        FileOutputStream fos2 = new FileOutputStream(file);*/
        for (int i = 97; i <= 101; i++) {
            // 将指定的字节写入此文件输出流
            fos.write(i);
        }
        // 将b.length字节数组写入此文件输出流
        // byte[] bytes = {97, 98, 99, 100, 101};
        byte[] bytes = "abcde".getBytes(StandardCharsets.UTF_8);
        fos.write(bytes);
        // 将bytes数组从指定的位置开始,以指定的偏移量开始迁移,将这些字符写入该文件输出流
        // write(byte[] b, int off, int len)
        // fos.write(bytes, 0, bytes.length);
        fos.write(bytes, 1, 3);
        // 释放资源
        fos.close();
    }
    

    这里写入方法中,写入的内容,都需要转换为ASCII码对应的形式,或者直接调用getBytes方法转换。

    ①字节流写数据如何实现换行:

    根据不同的系统,在写完数据的时候追加不同的换行符

    WIndows:\r\n LInux:\n Mac:\r

    // 写数据
    for (int i = 0; i < 10; i++) {
        fos.write("hello".getBytes());
        /*Windows:\r\n,Linux:\n,Mac:\r*/
        fos.write("\r\n".getBytes());
    }
    

    ②字节流写数据如何追加数据:

    字节流在写数据的时候,会将同名的文件内容清空

    我们使用下面这种构造方法创建文件输出流对象

    FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件,第二个参数为true,则从文件末尾追加数据而不是开头

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("D:\\Java\\java.txt", true);
        // 写数据
        for (int i = 0; i < 10; i++) {
            fos.write("hello".getBytes());
            /*Windows:\r\n,Linux:\n,Mac:\r*/
            fos.write("\r\n".getBytes());
        }
        // 释放资源
        fos.close();
    }
    

    字节流写数据的异常处理

    之前在写字节输出流的时候,有异常情况都是直接throws抛出,这个时候我们自己编写代码,捕获一场并进行处理操作

    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("D:\\Java\\java.txt", true);
            fos.write("World".getBytes());
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    字节流读数据(一次读一个数据)

    FileInputStream:从文件系统中的文件获取输入字节

    需求:把之前创建的java.txt文件中的数据读取出来在控制台输出

    FileInputStream(String name):通过打开与实际文件的连接创建一个FileInputStream对象,该文件由文件系统中的文件路径名+文件名命名

    public static void main(String[] args) throws IOException {
        // 创建字节流输入流对象
        FileInputStream fis = new FileInputStream("D:\\Java\\java.txt");
        // 调用字节输入流对象中的读取方法
        // int read() : 从字节输入流中读取一个字节的数据
        /*int read = fis.read();
        System.out.println(read);
        System.out.println((char) read);
        // 第二次读取数据(如果文件到达末尾,则返回-1)
        read = fis.read();*/
        /*int read = fis.read();
        while (read != -1) {
            System.out.print((char) read);
            read = fis.read();
        }*/
        int read;
        while ((read = fis.read()) != -1) {
            System.out.print((char) read);
        }
        System.out.println(read);
        System.out.println((char) read);
        // 关闭资源
        fis.close();
    }
    

    具体操作如上所示,最终打印的结果跟文件中的内容一致

    案例:复制文本文件

    需求:将文件D:\Java\java.txt文件复制到模块目录下

    分析:复制文本文件,其实就是将文本文件中的内容读取出来,写入到目的路径下的相同类型的文件中

    public static void main(String[] args) throws IOException {
        // 根据数据源创建字节输入流对象,读取操作
        FileInputStream fis = new FileInputStream("D:\\Java\\java.txt");
        // 根据目的地创建字节输出流对象,写入操作
        FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt");
        // 读取数据,一次性读入一个字节,一次性写入一个字节
        int by;
        while ((by = fis.read()) != -1) {
            fos.write(by);
        }
        // 释放资源
        fos.close();
        fis.close();
    }
    

    运行结果如下:在工作空间下的模块目录中新增了java.txt文件

    image-20210430203019769

    字节流读数据(一次读一个字节数组数据)

    把文件中的内容读取出来在控制台输出

    public static void main(String[] args) throws IOException {
        // 创建字节输入流对象
        FileInputStream fis =new FileInputStream("D:\\Java\\java.txt");
    
        // 调用字节输入流的读数据方法
        byte[] bytes = new byte[1024];
        /*int len = fis.read(bytes);
        System.out.println(len);
        System.out.println(new String(bytes, 0, len));*/
        int len;
        while ((len = fis.read(bytes)) != -1) {
            System.out.print(new String(bytes, 0, len));
        }
    
        // 释放资源
        fis.close();
    }
    

    最终读取出来的就是文件中的内容

    案例:复制图片

    public static void main(String[] args) throws IOException {
        // 根据数据源对象创建字节输入流对象
        FileInputStream fis = new FileInputStream("D:\\1.png");
        // 根据目的地创建字节输出流
        FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\1.png");
    
        // 复制操作
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read()) != -1) {
            fos.write(bytes, 0, len);
        }
    
        // 关闭资源
        fos.close();
        fis.close();
    }
    

    这样编写运行之后,图片就会从输入流的路径复制到输出流的路径下

    字节缓冲流

    image-20210430210639846

    使用过程

    public static void main(String[] args) throws IOException {
        // 创建缓冲字节输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\bos.txt"));
        // 写数据
        bos.write("hello\r\n".getBytes());
        bos.write("world\r\n".getBytes());
        // 释放资源
        bos.close();
        // 创建缓冲字节输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\bos.txt"));
        // 读取文件数据
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            System.out.print(new String(bytes, 0, len));
        }
        // 释放资源
        bis.close();
    }
    

    案例:复制视频

    需求:复制视频文件,将一个路径下的视频文件复制到另外的路径下

    public class CopyAviDemo {
        public static void main(String[] args) throws IOException {
            // 创建起始时间
            long startTime = System.currentTimeMillis();
    
            method1();
    
            // 结束时间
            long endTime = System.currentTimeMillis();
            // 一共耗时多少秒
            long totalTime = endTime - startTime;
            System.out.println("一共耗时:" + totalTime + "毫秒");
        }
    
        // 字节缓冲流一次读取一个字节数组
        private static void method4() throws IOException {
            // 字节缓冲流复制视频文件
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\demo.avi"));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi"))// 复制操作
            byte[] bytes = new byte[1024];
            int len;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
    
            // 关闭资源
            bos.close();
            bis.close();
        }
    
        // 字节缓冲流一次读取一个字节
        private static void method3() throws IOException {
            // 字节缓冲流复制视频文件
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\demo.avi"));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi"))// 复制操作
            int by;
            while ((by = bis.read()) != -1) {
                bos.write(by);
            }
    
            // 关闭资源
            bos.close();
            bis.close();
        }
    
        // 基本字节流一次读取一个字节数组
        private static void method2() throws IOException {
            // 字节流复制视频
            FileInputStream fis = new FileInputStream("D:\\Java\\demo.avi");
            FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi");
    
            // 复制操作
            byte[] bytes = new byte[1024];
            int len;
            while ((len = fis.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
    
            // 关闭资源
            fos.close();
            fis.close();
        }
    
    
        // 基本字节流一次读取一个字节
        private static void method1() throws IOException {
            // 字节流复制视频
            FileInputStream fis = new FileInputStream("D:\\Java\\demo.avi");
            FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi");
    
            // 复制视频操作
            int by;
            while ((by = fis.read()) != -1) {
                fos.write(by);
            }
    
            // 释放资源
            fos.close();
            fis.close();
    
        }
    }
    

    对比这四种方式,根据运行时间可以得到,字节缓冲流的速度大于字节流,读写字符数组的方式速度大于读写单个字节的速度

    字符流

    image-20210501145954622

    /*
    * 单个汉字在UTF-8编码下占3个字节,在GBK编码下占2个字节
    * */
    public static void main(String[] args) throws UnsupportedEncodingException {
        // String s = "abc"; // [97, 98, 99]
        String s = "中国"; // UTF-8:[-28, -72, -83, -27, -101, -67]
        // GBK:[-42, -48, -71, -6]
        byte[] bytes = s.getBytes("GBK");
        System.out.println(Arrays.toString(bytes));
    }
    

    编码表

    image-20210501151654340

    image-20210501151717326

    image-20210501151819302

    image-20210501152003084

    image-20210501152119765

    字符串中编码解码问题

    image-20210501152228740

    public static void main(String[] args) throws UnsupportedEncodingException {
        String s = "中国";
        byte[] bytes = s.getBytes(); // 默认使用UTF-8进行编码
        String ss = new String(bytes, "UTF-8");
        System.out.println(Arrays.toString(bytes));  // [-28, -72, -83, -27, -101, -67]
        System.out.println(ss);
        byte[] bytes1 = s.getBytes("GBK");
        String ss1 = new String(bytes1, "GBK");
        System.out.println(Arrays.toString(bytes1)); // [-42, -48, -71, -6]
        System.out.println(ss1);
    }
    

    使用何种方式编码,就要使用何种方式解码,否则将会出现乱码问题。

    字符流中的编码解码问题

    字符流的基类

    • Reader:字符输入流的抽象类
    • Writer:字符输出流的抽象类

    字符流中和编码解码相关的两个类

    • InputStreamReader
    • OutputStreamWriter
    public static void main(String[] args) throws IOException {
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt"), "GBK");
        osw.write("中国");
        osw.close();
        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt"), "GBK");
        int len;
        while ((len = isr.read()) != -1) {
            System.out.print((char) len);
        }
        isr.close();
    }
    

    字符流写数据的五种方式

    image-20210501182733661

    image-20210504133020515

    public static void main(String[] args) throws IOException {
        // 创建osw对象
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\osw.txt"));
        // 写数据,此时数据还在缓冲区
        osw.write(97);
        // 调用刷新流,将数据从缓冲区转移到目的文件中
        // osw.flush();
    
        // 写入一个数组
        char[] bytes = {'h', 'e', 'l', 'l', 'o'};
        osw.write(bytes/*, 0, bytes.length*/);
    
        // 写入一个字符串
        osw.write("world");
    
        // 写入一个字符串的一部分
        osw.write("java", 0, 1);
    
        // 关闭资源,但是在关闭之前会自动调用一次刷新
        osw.close();
    }
    

    字符流读数据的两种方式

    image-20210504133127153

    public static void main(String[] args) throws IOException {
        // 创建字符流读取对象
        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\osw.txt"));
        // 读取数据,一次读取一个字符
        int ch;
        while ((ch = isr.read()) != -1) {
            System.out.print((char) ch);
        }
        // 一次性读入一个字符串
        char[] chs = new char[1024];
        int len;
        while ((len = isr.read(chs)) != -1) {
            System.out.print(new String(chs, 0, len));
        }
        // 释放资源
        isr.close();
    }
    

    案例:复制java文件

    public static void main(String[] args) throws IOException {
        // 根据源文件创建字符输入流
        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
        // 根据目的地创建字符输出流
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\ConversionStringDemo.java"));
    
        // 复制文件操作
        /*int ch;
        while ((ch = isr.read()) != -1) {
            osw.write(ch);
        }*/
    
        char[] chs = new char[1024];
        int len;
        while ((len = isr.read(chs)) != -1) {
            osw.write(chs);
        }
    
        // 释放资源
        osw.close();
        isr.close();
    }
    

    案例:复制Java文件(改进版)

    image-20210504141549380

    public static void main(String[] args) throws IOException {
        // 根据源文件创建输入流对象
        FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java");
        // 根据目的地创建输出流对象
        FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\ConversionStringDemo.java");
    
        // 读写数据,复制文件
        int ch;
        while ((ch = fr.read()) != -1) {
            fw.write(ch);
        }
    
        char[] chars = new char[1024];
        int len;
        while ((len = fr.read(chars)) != -1) {
            fw.write(chars, 0, len);
        }
    
        // 释放资源
        fw.close();
        fr.close();
    }
    

    字符缓冲流

    image-20210507125006043

    public static void main(String[] args) throws IOException {
        /*BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bw.txt"));
        bw.write("hello\r\n");
        bw.write("world\r\n");*/
        
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bw.txt"));
    
        // 一次读取一个字符
        /*int ch;
        while ((ch = br.read()) != -1) {
            System.out.print((char) ch);
        }*/
    
        // 一次读取一个字符数组
        char[] chs = new char[1024];
        int len;
        while ((len = br.read(chs)) != -1) {
            System.out.print(new String(chs, 0, len));
        }
    
        // 关闭资源
        br.close();
        // bw.close();
    }
    

    案例:复制Java文件

    image-20210507130025862

    public static void main(String[] args) throws IOException {
        // 创建字符缓冲流读取对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
        // 创建字符缓冲流写入对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\Copy.java"));
        // 一次读入一个字符
        /*int ch;
        while ((ch = br.read()) != -1) {
            bw.write(ch);
        }*/
        // 一次读取一个字符数组
        char[] chs = new char[1024];
        int len;
        while ((len = br.read(chs)) != -1) {
            bw.write(chs, 0, len);
        }
    
        // 释放资源
        bw.close();
        br.close();
    }
    

    字符缓冲流特有功能

    image-20210507130849566

    public static void main(String[] args) throws IOException {
        /*BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bwDemo.txt"));
    
        for (int i = 1; i <= 10; i++) {
            bw.write("hello" + i);
            // bw.write("\r\n");
            bw.newLine();
            bw.flush();
        }
    
        // 释放资源
        bw.close();*/
    
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bwDemo.txt"));
    
        /*String line = br.readLine();
        System.out.println(line);
        line = br.readLine();
        System.out.println(line);
        line = br.readLine();
        System.out.println(line);
        line = br.readLine();
        System.out.println(line);
        // 当最终没有数据的时候,会输出null*/
    
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    
        // 释放资源
        br.close();
    }
    

    案例:复制Java文件(字符缓冲流特有功能实现)

    image-20210507132437294

    public static void main(String[] args) throws IOException {
        // 根据数据源创建字符输入流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
        // 根据目的源创建字符输入流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\Copy.java"));
        // 读写数据,复制文件
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        // 释放资源
        bw.close();
        br.close();
    }
    

    IO流小结

    image-20210507133726089

    image-20210507133759490

    案例:集合到文件

    需求:把ArrayList集合中的数据写入到文本文件中;要求:每一个字符串元素作为文件中的一行数据

    public static void main(String[] args) throws IOException {
        // 创建ArrayList对象
        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList.add("Hello");
        arrayList.add("World");
        arrayList.add("Java");
        // 创建字符缓冲输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\ArrayList.txt"));
        for (String line : arrayList) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        // 释放资源
        bw.close();
    }
    

    案例:文件到集合

    需求:与上述案例相反,将文件中的内容输出到集合中。

    public static void main(String[] args) throws IOException {
        // 创建字符缓冲输入流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\ArrayList.txt"));
        // 创建 ArrayList 集合对象
        ArrayList<String> arrayList = new ArrayList<String>();
        // 遍历文件,得到文本数据
        String line;
        while ((line = br.readLine()) != null) {
            arrayList.add(line);
        }
        // 释放资源
        br.close();
        // 遍历集合
        for (String s : arrayList) {
            System.out.println(s);
        }
    }
    

    案例:点名器

    需求:有一个文件中存储着班级同学的姓名,每一个姓名占一行,要求通过程序实现随机点名器

    实现的思路:将文件中的姓名输入到集合中,之后在集合的范围中产生随机数作为索引,访问到相对应的同学姓名

    public static void main(String[] args) throws IOException {
        // 创建字符缓冲输入流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\names.txt"));
        // 创建 ArrayList 集合对象
        ArrayList<String> arrayList = new ArrayList<String>();
        // 读写文件数据,写入集合
        String line;
        while ((line = br.readLine()) != null) {
            arrayList.add(line);
        }
        // 释放资源
        br.close();
        // 使用随机数对象产生随机数,范围为[0,arraylist.size())
        Random random =  new Random();
        int index = random.nextInt(arrayList.size());
        // 利用索引找到集合中对应的元素
        String name = arrayList.get(index);
        // 输出随机抽取到的姓名
        System.out.println(name);
    }
    

    案例:集合到文件(改进版)

    需求:把ArrayList中的学生数据写入到文本文件,要求一个学生信息在同一行的位置

    格式:学号,姓名,年龄,居住地

    public static void main(String[] args) throws IOException {
        // 创建集合对象
        ArrayList<Student> array = new ArrayList<Student>();
        // 创建学生对象
        Student s1 = new Student("1840800", "张三", 19, "北京");
        Student s2 = new Student("1840801", "张三", 20, "天津");
        Student s3 = new Student("1840802", "张三", 21, "上海");
        Student s4 = new Student("1840803", "张三", 22, "重庆");
        // 把学生对象添加进集合
        array.add(s1);
        array.add(s2);
        array.add(s3);
        array.add(s4);
        // 创建字符缓冲输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_09\\Students.txt"));
        // 遍历集合,得到每一个学生对象
        for (Student student : array) {
            // 把学生对象数据拼接成指定格式的字符串
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(student.getStuId()).append(",").append(student.getName()).append(",").append(student.getAge()).append(",").append(student.getAddress());
            // 调用字符缓冲输出流对象写数据
            bw.write(stringBuilder.toString());
            bw.newLine();
            bw.flush();
        }
        // 释放资源
        bw.close();
    }
    

    案例:文本到集合(改进版)

    需求:将文本文件中的数据读取出来写入到集合中并实现遍历

    要求每一行数据是一个对象的数据

    public static void main(String[] args) throws IOException {
        // 首先创建字符缓冲输入流
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_09\\Students.txt"));
        // 根据需求创建 ArrayList 集合对象
        ArrayList<Student> array = new ArrayList<Student>();
        // 读取文本数据,读取出对象内容
        String line;
        while ((line = br.readLine()) != null) {
            // 用 Split 方法分割读取到的字符串
            String[] split = line.split(",");
            // 格式:学号,姓名,年龄,住址
            Student student = new Student(split[0], split[1], Integer.parseInt(split[2]), split[3]);
            array.add(student);
        }
        // 释放资源
        br.close();
        // 遍历集合对象
        for (Student student : array) {
            System.out.println(student);
        }
    }
    

    案例:集合到文件(数据排序改进版)

    需求:键盘录入学生信息(姓名,语文成绩,数学成绩,英语成绩),要求将学生成绩按照总分降序排进文本文件

    格式:姓名,语文成绩,数学成绩,英语成绩

    思路:创建TreeSet对象实现

    步骤一:创建Student类

    public class Student {
        private String name;
        private int Chinese;
        private int Mathematics;
        private int English;
    
        public int getSum() {
            return this.Chinese + this.Mathematics + this.English;
        }
    
        public Student(String name, int chinese, int mathematics, int english) {
            this.name = name;
            Chinese = chinese;
            Mathematics = mathematics;
            English = english;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getChinese() {
            return Chinese;
        }
    
        public void setChinese(int chinese) {
            Chinese = chinese;
        }
    
        public int getMathematics() {
            return Mathematics;
        }
    
        public void setMathematics(int mathematics) {
            Mathematics = mathematics;
        }
    
        public int getEnglish() {
            return English;
        }
    
        public void setEnglish(int english) {
            English = english;
        }
    }
    

    步骤二:创建主类

    public class TreeSetToFile {
        public static void main(String[] args) throws IOException {
            // 创建 TreeSet 集合对象
            TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
                @Override
                public int compare(Student s1, Student s2) {
                    // 主要条件:总分是否相同
                    int num = s2.getSum() - s1.getSum();
                    // 次要条件:科目分数是否相同
                    int num2 = num == 0 ? s2.getChinese() - s1.getChinese() : num;
                    int num3 = num2 == 0 ? s2.getMathematics() - s1.getMathematics() : num2;
                    // 次要条件:姓名是否相同
                    int num4 = num3 == 0 ? s2.getName().compareTo(s1.getName()) : num3;
                    return num4;
                }
            });
            // 从键盘录入学生数据
            for (int i = 0; i < 5; i++) {
                Scanner sc = new Scanner(System.in);
                System.out.println("请录入第" + (i + 1) + "个学生信息:");
                System.out.println("请输入姓名:");
                String name = sc.nextLine();
                System.out.println("语文成绩:");
                int Chinese = sc.nextInt();
                System.out.println("数学成绩:");
                int Mathematics = sc.nextInt();
                System.out.println("英语成绩:");
                int English = sc.nextInt();
                // 创建学生对象
                Student student = new Student(name, Chinese, Mathematics, English);
                // 把学生对象添加进集合
                ts.add(student);
            }
    
            // 创建字符缓冲输出流对象
            BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_10\\ts.txt"));
    
            // 遍历学生对象,把学生对象的数据拼接成指定格式的字符串内容
            for (Student student : ts) {
                // 格式:姓名,语文成绩,数学成绩,英语成绩
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append(student.getName()).append(",").append(student.getChinese()).append(",").append(student.getMathematics()).append(",").append(student.getEnglish()).append(", 总分:").append(student.getSum());
                // 调用字符缓冲输出流对象写数据
                bw.write(stringBuilder.toString());
                bw.newLine();
                bw.flush();
            }
    
            // 释放资源
            bw.close();
        }
    }
    

    案例:复制单级文件夹

    image-20210508162805260

    复制单级文件夹,但是由于文件夹中的文件不是单一格式,所以我们采用字节流复制文件

    public class CopySingleFileFolder {
        public static void main(String[] args) throws IOException {
            // 创建源文件夹对象
            File srcFile = new File("D:\\temp");
            // 获取文件夹名称
            String srcName = srcFile.getName();
            // 创建目的文件夹对象
            File destFolder = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_11", srcName);
            // 判断文件夹对象是否存在
            if (! destFolder.exists()) {
                destFolder.mkdir();
            }
    
            // 获取数据源目录下的所有文件对象
            File[] listFiles = srcFile.listFiles();
    
            // 遍历 listFiles 数组,将文件写入目的文件夹中
            for (File file : listFiles) {
                // 获取源文件的名称
                String srcFileName = file.getName();
                // 创建目的 File 对象
                File destFile = new File(destFolder, srcFileName);
                // 复制文件
                copyFile (srcFile, destFile);
            }
        }
    
        private static void copyFile(File srcFile, File destFile) throws IOException {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
    
            byte[] bytes = new byte[1024];
            int len;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
    
            bos.close();
            bis.close();
        }
    }
    

    案例:复制多级文件夹

    需求:复制多级文件夹,该文件夹中可能包含子文件夹,子文件夹中包含其他文件

    public class CopyMultiFileFolder {
        public static void main(String[] args) throws IOException {
            // 创建数据源 File 目录对象
            File srcFile = new File("D:\\temp");
            // 创建目的 File 对象
            File destFile = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_11\\temp_1");
    
            // 调用方法复制文件夹中的内容
            copyFolder (srcFile, destFile);
        }
    
        // 复制文件夹方法
        private static void copyFolder(File srcFile, File destFile) throws IOException {
            // 判断是否是文件夹
            if (srcFile.isDirectory()) {
                // 在目的地创建和数据源 File 一样的文件名称
                String srcFileName = srcFile.getName();
                File newFolder = new File(destFile, srcFileName);
                if (! newFolder.exists()) {
                    newFolder.mkdir();
                }
                // 获取数据源对象中的所有文件
                File[] listFiles = srcFile.listFiles();
                for (File listFile : listFiles) {
                    copyFile(listFile, newFolder);
                }
            } else {
                // 不是文件夹,是文件,直接复制
                File newFile = new File(destFile, srcFile.getName());
                copyFile(srcFile, newFile);
            }
    
        }
    
        // 字节缓冲流复制文件
        private static void copyFile(File srcFile, File destFile) throws IOException {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
    
            byte[] bytes = new byte[1024];
            int len;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
    
            bos.close();
            bis.close();
        }
    }
    

    复制文件的异常处理

    复制文件中,对于可能出现异常情况的处理方案:

    一共有以下四种:

    public class CopyFileException {
    
        public static void main(String[] args) throws IOException {
            method1();
            method2();
            method3();
            method4();
        }
    
        // 四、JDK9 对于 JDK7 方案的改进办法
        private static void method4 () throws IOException {
            // 此种写法最后会自动释放资源
            FileReader fr = new FileReader("fr.txt");
            FileWriter fw = new FileWriter("fw.txt");
            try (fr;fw) {
                char[] chars = new char[1024];
                int len;
                while ((len = fr.read(chars)) != -1) {
                    fw.write(chars, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 三、JDK7 出现的改进方法
        private static void method3 () {
            // 此种写法最后会自动释放资源
            try (FileReader fr = new FileReader("fr.txt");
                 FileWriter fw = new FileWriter("fw.txt");) {
                char[] chars = new char[1024];
                int len;
                while ((len = fr.read(chars)) != -1) {
                    fw.write(chars, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 二、使用 Try-Catch 语句块捕获异常
        private static void method2 () {
            FileReader fr = null;
            FileWriter fw = null;
            try {
                fr = new FileReader("fr.txt");
                fw = new FileWriter("fw.txt");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            char[] chars = new char[1024];
            int len;
            try {
                while ((len = fr.read(chars)) != -1) {
                    fw.write(chars, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            try {
                fw.close();
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 一、直接抛出处理
        private static void method1 () throws IOException {
            FileReader fr = new FileReader("fr.txt");
            FileWriter fw = new FileWriter("fw.txt");
    
            char[] chars = new char[1024];
            int len;
            while ((len = fr.read(chars)) != -1) {
                fw.write(chars, 0, len);
            }
    
            fw.close();
            fr.close();
        }
    }
    

    特殊操作流

    标准输入输出流

    System类中有两个标准的输入输出流,都是静态成员变量

    • public static final InputStream in:标准输入流。通常该流对应于键盘输入或由主机环境或用户指定的另一个输入源

    • public static final OutputStream out:标准输出流。通常该流对应于显示输出或由主机环境或用户指定的另一个输出目标

    标准输入流

    自己实现键盘录入数据:

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    

    调用Scanner类实现键盘录入数据:

    Scanner sc = new Scanner(System.in);
    

    实现代码:

    public static void main(String[] args) throws IOException {
        // 使用多态的方式创建 标准输入流 对象
        /*InputStream is = System.in;
        // 字节流读取数据
        int by;
        while ((by = is.read()) != -1) {
            System.out.print((char) by);
        }
        // 释放资源
        is.close();*/
    
        // 上述代码不能实现中文的正常输出显示,转换为字符流实现
        /*InputStreamReader isr = new InputStreamReader(is);
        // 实现一行文字的读取,我们要转换成 字符缓冲流 实现
        BufferedReader br = new BufferedReader(isr);*/
    
        // 整合成一行代码,格式如下:
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    
        System.out.println("请输入一行文字:");
        String line = br.readLine();
        System.out.println(line);
    
        System.out.println("请输入一个整数:");
        int i = Integer.parseInt(br.readLine());
        System.out.println(i);
    
        // 但是上述实现过程太过复杂,我们直接使用 Scanner
        Scanner sc = new Scanner(System.in);
    }
    

    标准输出流

    标准输出流本质上是PrintStream,也就是说PrintStream所具备的方法,System.out中也有,也可以直接调用

    自己实现控制台打印输出:

    public static void main(String[] args) {
        PrintStream ps = System.out;
    
        // 调用打印方法
        ps.print(100);
        ps.print("Hello World");
    
        // 调用换行打印
        ps.println(100);
        ps.println("Hello World");
        
        // 直接调用
        System.out.println(100);
        System.out.println("Hello World");
        
        // 换行打印方法可以无参数,但是 print 方法不能没有参数
        ps.println();
        // ps.print();
    }
    

    打印流

    打印流只负责打印输出数据,不负责读取数据,有自己特有的方法

    • 字节打印流:PrintStream
    • 字符打印流:PrintWriter

    字节打印流

    创建字节打印流对象:

    PrintStream ps = new PrintStream(fileName);
    

    使用字节打印流写数据:

    调用父类的方法write()会转码输入数据;如果调用自己特有方法print()/println()写数据,则会原样写入数据

    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\print\\ps.txt");
    
        // 使用普通方法写数据
        ps.write(97); // 会自动转码成 ASCII 对应的字母 a
    
        // 使用特有的方法写数据
        ps.print(97);   // 直接写进的就是数字 97
        ps.println(98); // 换行写数据,末尾添加换行符
        ps.println(99);
    
        // 释放资源
        ps.close();
    }
    

    字符打印流

    image-20210508200415410

    第二种方式创建PrintWriter对象,会自动执行刷新,将缓冲区的数据读取出来

    public static void main(String[] args) throws IOException {
        String fileName = "D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\print\\pw.txt";
        PrintWriter pw1 = new PrintWriter(fileName);
        // 写数据
        pw1.print("Hello1");
        pw1.println();
        pw1.flush();
        pw1.print("World1");
        // 释放资源,此步骤会实现自动刷新
        pw1.close();
    
        // 以这种方式创建字符输出流,会自动刷新
        PrintWriter pw2 = new PrintWriter(new FileWriter(fileName), true);
        // 写数据,此步骤不释放资源,自动刷新执行,数据写入
        pw2.println("Hello2");
        pw2.println("World2");
    }
    

    案例:复制Java文件(打印流实现)

    需求:利用打印流实现Java文件的复制

    public static void main(String[] args) throws IOException {
        /*// 根据数据源创建 缓冲输入流 对象
        BufferedReader br = new BufferedReader(new FileReader("day07_IOStream\\src\\print\\PrintStreamDemo.java"));
        // 根据目的地创建 缓冲输出流 对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("day07_IOStream\\src\\print\\PrintStreamDemo_Copy.java"));
        // 读写数据,复制文件
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
        // 释放资源
        bw.close();
        br.close();*/
    
        // 根据数据源创建 缓冲输入流 对象
        BufferedReader br = new BufferedReader(new FileReader("day07_IOStream\\src\\print\\PrintStreamDemo.java"));
        // 根据目的地创建 打印输出流 对象
        PrintWriter pw = new PrintWriter(new FileWriter("day07_IOStream\\src\\print\\PrintStreamDemo_Copy.java"), true);
        // 读写数据
        String line;
        while ((line = br.readLine()) != null) {
            pw.println(line);
        }
        // 释放资源
        pw.close();
        br.close();
    }
    

    相比于原方法,使用起来更加简单,执行更加高效

    对象序列化流

    image-20210508202806414

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o82ZXbH2-1639662744855)(https://i.loli.net/2021/05/08/t2Djv5xiSCcBKWY.png)]

    对象序列化就是将对象的相关信息,存储到指定的文件中;等到需要重构对象的时候,再通过对文件调用对象反序列化流实现。

    步骤一:在创建对象序列化对象的时候,首先要创建对象

    public class Student implements Serializable {
        private String name;
        private int age;
        // 对应的无参构造和全参构造,以及 Getter and Setter 方法
    }
    

    步骤二:创建对象序列化流对象,将指定对象进行序列化

    public class ObjectOutputStreamDemo {
        /*
        * NotSerializableException : 当一个实例需要实现Serializable接口。
        * 序列化运行时或实例类可以抛出此异常,参数应该是类的名称
        * Serializable : 一个类的串行化是由类实现java.io.serializable接口启用。
        * 类没有实现这个接口不会有任何序列化或反序列化其状态。
        * 序列化接口没有任何方法或字段只能识别可序列化的语义。
        * */
        public static void main(String[] args) throws IOException {
            // 创建 对象序列化流 对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
            // 创建对象
            Student s = new Student("雨下一整晚", 20);
            // 执行对象序列化
            oos.writeObject(s);
            // 释放资源
            oos.close();
        }
    }
    

    这个时候创建的新的oos.txt中的内容大部分是乱码,内容只有少部分信息可以看出。

    这个时候我们要通过对象的反序列化流将oos.txt中的内容读取出来并创建相关的对象。

    对象反序列化流

    image-20210508210419862

    需求:将之前的序列化后的对象文件实现 反序列化输出 创建原有的对象

    /*
    * ObjectInputStream(InputStream in) : 创建一个对象输入流读取从指定的输入流。
    * */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 创建对象反序列化流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
        // 从已有的文件中读取数据,将对象反序列化
        // readObject() : 从对象输入流对象
        Object object = ois.readObject();
        // 转成对应的序列化之前的对象
        Student student = (Student) object;
        System.out.println(student.getName() + "," + student.getAge());
        // 释放资源
        ois.close();
    }
    

    相关问题

    一、如果序列化后的类文件被修改之后,读取数据会不会出现问题?

    类文件,也就是之前的Student类被修改,那么会不会出问题?

    public class ObjectStreamDemo {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            // write();
            read();
        }
    
        // 序列化
        private static void write () throws IOException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
            Student s = new Student("雨下一整晚Real", 20);
            oos.writeObject(s);
            oos.close();
        }
    
        // 反序列化
        private static void read () throws IOException, ClassNotFoundException {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
            Object object = ois.readObject();
            Student student = (Student) object;
            System.out.println(student.getName() + "," + student.getAge());
            ois.close();
        }
    }
    

    这个时候,我们首先调用write()方法,将对象进行序列化之后;我们修改Student类的代码,添加一个toString()方法,之后只调用read()方法,这个时候出现了异常

    Exception in thread “main” java.io.InvalidClassException:
    object_serialization_stream.Student;
    local class incompatible: stream classdesc serialVersionUID = 1337395739814197595,
    local class serialVersionUID = -2611514023222870104

    当序列化运行时检测到一个类中的下列问题之一时抛出:

    • 类的串行版本与从流中读取的类的不匹配
    • 该类包含未知的数据类型
    • 类中没有一个可访问的无参数构造函数

    通过观察异常产生的原因可以得知,是由于串行版本不一致导致的异常

    序列化运行时与每个可序列化的类关联一个版本号,被称为serialVersionUID,用于在反序列化期间验证发送方和接收者有序列化对象,对象是相对于序列化兼容加载的类。如果接收者具有比相应的类的对象发送不同的serialVersionUID加载了一个类,然后反序列化将导致InvalidClassException

    二、这种问题应该如何解决?

    给序列化对象所属的类添加一个值:

    private static final long serialVersionUID = 42L;
    

    三、如果某个对象中的某个值不想被序列化,应该如何实现?

    给该属性添加关键字 transient —— adj.短暂的;转瞬即逝的;倏忽;暂住的;过往的;临时的

    // private int age;
    private transient int age;
    

    Properties

    简单使用,和之前的集合的使用大致相同

    注意:创建的时候不用写泛型

    public static void main(String[] args) {
        // 创建集合对象
        Properties properties = new Properties();
        // 存储对象元素
        properties.put("001", "张三");
        properties.put("002", "李四");
        properties.put("003", "王五");
        // 遍历对象元素
        Set<Object> keySet = properties.keySet();
        for (Object key : keySet) {
            Object value = properties.get(key);
            System.out.println(key + "," + value);
        }
    }
    

    Properties作为集合的特有方法:

    image-20210508223144530

    public static void main(String[] args) {
        // 创建集合对象
        Properties prop = new Properties();
        // 调用 setProperties() 方法
        prop.setProperty("001", "张三");
        prop.setProperty("002", "李四");
        prop.setProperty("003", "王五");
        // 调用 getProperties() 方法
        String property = prop.getProperty("001");
        System.out.println(property);
        // 调用 stringPropertyNames() 获得键名
        Set<String> keySet = prop.stringPropertyNames();
        for (String value : keySet) {
            String s = prop.getProperty(value);
            System.out.println(value + "," + s);
        }
    }
    

    Properties和IO流结合的方法

    image-20210508224042428

    public class PropertiesDemo {
        public static void main(String[] args) throws IOException {
            // 把集合中的数据保存到文件
            myStore();
            // 把文件中的数据加载到集合
            myLoad();
        }
    
        private static void myLoad() throws IOException {
            Properties properties = new Properties();
            // 加载文件中的数据到集合中
            FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\Properties.txt");
            properties.load(fr);
            fr.close();
            System.out.println(properties);
        }
    
        private static void myStore() throws IOException {
            Properties properties = new Properties();
            // 往集合中添加数据
            properties.put("001", "张三");
            properties.put("002", "李四");
            properties.put("003", "王五");
            // 将集合中的数据写入文件
            FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\Properties.txt");
            properties.store(fw, null);
            fw.close();
        }
    }
    

    案例:游戏次数

    需求:实现猜数字小游戏,每人只能玩三次;如果还想玩,提示:试玩已结束

    image-20210508225458225

    步骤一:创建游戏类 GuessNumber

    public class GuessNumber {
        public GuessNumber() {
        }
    
        // 猜数字游戏
        public static void start () {
            // 要完成猜数字的游戏,首先要有一个要猜的数字,使用随机数生成,范围0-100
            Random random = new Random();
            int number = random.nextInt(100) + 1;
    
            while (true) {
                // 使用程序实现猜数字,每次均要实现键盘输入
                Scanner sc = new Scanner(System.in);
                System.out.println("请输入你的答案:");
                int guessNumber = sc.nextInt();
    
                // 比较输入的数字和系统产生的数字的大小,根据大小输出相应的提示
                if (guessNumber > number) {
                    System.out.println("你猜的数字" + guessNumber + "大了");
                } else if (guessNumber < number) {
                    System.out.println("你猜的数字" + guessNumber + "小了");
                } else {
                    System.out.println("恭喜你猜对了!");
                    break;
                }
            }
        }
    }
    

    步骤二:创建game.txt文件

    #Sat May 08 23:14:11 CST 2021
    count=3
    

    步骤三:编写判断主方法

    public class PropertiesGuessNumber {
        public static void main(String[] args) throws IOException {
            // 创建集合对象
            Properties properties = new Properties();
    
            // 从文件中读取数据
            FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\game.txt");
            properties.load(fr);
            fr.close();
    
            // 通过 properties 集合获取到 count 的值
            String count = properties.getProperty("count");
            int number = Integer.parseInt(count);
    
            // 判断游戏运行的次数,并随着玩游戏的次数的增加,将 count 的值修改写入
            if (number >= 3) {
                // 如果次数到了,则提示试玩已结束
                System.out.println("试玩已结束!");
            } else {
                // 调用游戏开始方法
                GuessNumber.start();
                number ++;
                properties.setProperty("count", String.valueOf(number));
                // 创建 字符缓冲流 存储 count 值
                FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\game.txt");
                properties.store(fw, null);
                fw.close();
            }
        }
    }
    

    当game.txt文件中的count = 3的时候,会提示“试玩已结束!”

    七、多线程

    实现多线程

    进程和线程

    进程:是系统正在运行的程序,

    • 系统进行资源分配和独立调用的基本单位;
    • 每一个进程都有它自己的内存空间和系统资源;

    线程:是进程中的单个顺序控制流,是一条执行路径

    • 单线程:一个进程中只有一条执行路径,则称为单线程程序
    • 多线程:一个进程中如果有多条执行路径,则成为多线程程序

    实现多线程

    方式一:继承Thread

    1. 创建MyThread类继承Thread
    2. 重写Thread类中的run()方法
    3. 创建MyThread类对象
    4. 启动线程
    public class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(i);
            }
        }
    }
    
    public class MyThreadDemo {
        public static void main(String[] args) {
            MyThread myThread1 = new MyThread();
            MyThread myThread2 = new MyThread();
            // 直接调用run方法并没有启动多线程,需要调用start方法启动多线程
            /*myThread1.run();
            myThread2.run();*/
            myThread1.start();
            myThread2.start();
        }
    }
    

    注意:

    一、为什么要重写run()方法?

    因为run方法就是多线程在执行的时候需要被执行的内容,run()封装了被线程执行的代码

    二、run()start()方法有什么区别?

    run():封装线程被执行的代码,直接调用,相当于普通方法的调用

    start():启动线程,然后由JVM调用该线程的run()方法

    设置和获取线程名称

    • 设置线程名称方法 void setName(String name)将此线程的名称更改为参数中的值
    • 获取线程名称 String getName() 返回此线程的名称
    • 返回当前正在执行的线程对象的引用:public static Thread currentThread()
    public class MyThread extends Thread{
    
        public MyThread() {
    
        }
    
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    }
    
    
    
    /*
    * Thread中本身有一个名为name的成员变量
    * private volatile String name;
    *
    无参构造方法
    public Thread() {
        this(null, null, "Thread-" + nextThreadNum(), 0);
    }
    带参构造方法,可以在自己定义的类中添加无参构造之后再自己定义带参构造设置名字
    public Thread(String name) {
        this(null, null, name, 0);
    }
    全参构造方法
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize, boolean inheritThreadLocals) {
        this(group, target, name, stackSize, null, inheritThreadLocals);
    }
    获取线程名字方法
    public final String getName() {
        return name;
    }
    设置名字方法
    public final synchronized void setName(String name) {
            checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
    
        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }
    初始化名字参数:
    private static int threadInitNumber; 初始化值为0
    private static synchronized int nextThreadNum() {
        return threadInitNumber++; 自动添加数值,返回当前值之后+1操作
    }
    
    * */
    
    public class MyThreadDemo {
        public static void main(String[] args) {
            /*MyThread mt1 = new MyThread();
            MyThread mt2 = new MyThread();
            mt1.setName("线程1");
            mt2.setName("线程2");
            mt1.start();
            mt2.start();*/
    
            MyThread mt1 = new MyThread("线程1");
            MyThread mt2 = new MyThread("线程2");
    
            mt1.start();
            mt2.start();
    
            // public static Thread currentThread()
            // 返回当前正在执行的线程对象的引用
            System.out.println(Thread.currentThread().getName());
        }
    }
    

    线程调度

    线程调度有两种模式

    • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
    • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

    Java所使用的是抢占式调度模型。所以多线程的程序执行具有随机性,因为谁抢占到CPU的使用权是不一定的。

    Thread类中获取线程优先级以及设置线程优先级的方法:

    • public final int getPriority()返回此线程的优先级
    • public final void setPriority(int newPriority)更改此线程的优先级

    线程优先级的范围是1-10,默认线程优先级是5;线程优先级高仅仅只是线程获得时间片的概率高,并不是线程一定能够每次都抢占到时间片。可能需要在多次运行之后,才能看到想要的结果。

    public class ThreadPriority extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    }
    
    public class ThreadPriorityDemo {
        public static void main(String[] args) {
            ThreadPriority priority1 = new ThreadPriority();
            ThreadPriority priority2 = new ThreadPriority();
            ThreadPriority priority3 = new ThreadPriority();
    
            priority1.setName("飞机");
            priority2.setName("高铁");
            priority3.setName("火车");
    
            System.out.println(priority1.getPriority());  // 5
            System.out.println(priority2.getPriority());  // 5
            System.out.println(priority3.getPriority());  // 5
    
    
            // IllegalArgumentException : 如果优先级不在范围 MIN_PRIORITY到 MAX_PRIORITY
            // priority1.setPriority(10000);
    
            System.out.println(Thread.MIN_PRIORITY);    // 1
            System.out.println(Thread.MAX_PRIORITY);    // 10
            System.out.println(Thread.NORM_PRIORITY);   // 5
    
            // 线程优先级高仅仅表示获取到执行权限的概率更高,并不是每次都能获取执行
            priority1.setPriority(1);
            priority2.setPriority(5);
            priority3.setPriority(10);
    
            priority1.start();
            priority2.start();
            priority3.start();
        }
    }
    

    线程控制

    image-20210522192936473

    首先创建对应的线程类,代码如下所示:

    public class ThreadSleep extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class ThreadSleepDemo {
        public static void main(String[] args) {
            ThreadSleep ts1 = new ThreadSleep();
            ThreadSleep ts2 = new ThreadSleep();
            ThreadSleep ts3 = new ThreadSleep();
    
            ts1.setName("曹操");
            ts2.setName("刘备");
            ts3.setName("孙权");
    
            ts1.start();
            ts2.start();
            ts3.start();
        }
    }
    

    Thread.sleep()的作用是让线程进行休眠,参数为指定的休眠时间。

    创建线程类ThreadDemo,之后通过创建实例对象,对两个方法进行验证使用。

    public class ThreadDemo extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    }
    

    Thread.join()方法,是等待线程结束。

    public class ThreadJoinDemo {
        public static void main(String[] args) throws InterruptedException {
            ThreadJoin tj1 = new ThreadJoin();
            ThreadJoin tj2 = new ThreadJoin();
            ThreadJoin tj3 = new ThreadJoin();
    
            tj1.setName("Join1");
            tj2.setName("Join2");
            tj3.setName("Join3");
    
            tj1.start();
            // join() 等待该线程死亡
            tj1.join();
            tj2.start();
            tj3.start();
        }
    }
    

    Thread.setDaemon()设置守护线程,当前所运行的线程全为守护线程的时候,Java虚拟机将退出。

    public class ThreadDaemonDemo {
        public static void main(String[] args) {
            ThreadDaemon td1 = new ThreadDaemon();
            ThreadDaemon td2 = new ThreadDaemon();
    
            td1.setName("关羽");
            td2.setName("张飞");
    
            // 设置主线程
            Thread.currentThread().setName("刘备");
    
            // 设置守护线程,在主线程结束之后立刻结束
            // setDaemon(boolean on) 标志着该线程是daemon线程或用户线程
            // 当运行的线程都是守护线程的时候,Java虚拟机将退出
            td1.setDaemon(true);
            td2.setDaemon(true);
    
            td1.start();
            td2.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    

    线程生命周期

    image-20210524003100680

    多线程的实现方法

    有两种方法来创建一个新的执行线程,一是声明一个类是一类Thread。这类应重写类Thread的run方法,子类的一个实例可以被分配和启动。创建一个线程的另一个方式是声明一个类实现Runnable接口,该类实现run方法。然后可以分配该类的实例,在创建Thread时作为参数传递并启动。

    方式二:实现Runnable接口

    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,将MyRunnable对象作为构造方法的参数
    • 启动线程
    public class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    
    public class MyRunnableDemo {
        public static void main(String[] args) {
            MyRunnable mr = new MyRunnable();
    
            // Thread(Runnable target) 分配一个新的 Thread 对象
            Thread tr1 = new Thread(mr);
            Thread tr2 = new Thread(mr);
            /*tr1.start();
            tr2.start();*/
    
            // Thread(Runnable target, String name) 分配一个新的 Thread对象
            Thread tr3 = new Thread(mr, "火车");
            Thread tr4 = new Thread(mr, "高铁");
            tr3.start();
            tr4.start();
        }
    }
    

    相比于直接继承自Thread类,实现Runnable接口的好处:

    • 避免了Java的单继承,在实现多线程的时候还可以再继承自另一个接口或者类
    • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好地体现了面向对象的设计思想

    线程同步

    案例:多窗口售卖电影票,总票数100

    image-20210524172442855

    实现代码如下所示:

    public class SellTicket implements Runnable {
    
        private int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }
    
    public class SellTicketDemo {
        public static void main(String[] args) {
            SellTicket st = new SellTicket();
    
            Thread th1 = new Thread(st, "窗口1");
            Thread th2 = new Thread(st, "窗口2");
            Thread th3 = new Thread(st, "窗口3");
            th1.start();
            th2.start();
            th3.start();
        }
    }
    

    思考

    现实生活中,卖票也是需要时间的;反映在程序中,我们给每一次的卖票过程中添加一个Sleep方法,每一次卖票让线程休息100ms。

    修改run方法,如下:

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
    

    这样引发的运行结果会出现问题:①同一张的票出现多次;②出现负数编号的票

    窗口1正在出售第100张票
    窗口3正在出售第100张票
    窗口2正在出售第100张票

    窗口3正在出售第0张票
    窗口2正在出售第-1张票

    问题的原因主要是线程执行的随机性,分析过程如下:

    image-20210524174413883

    线程数据安全

    上述案例中的ticket变量之所以会出现不合理的情况,是因为同一时刻被多个线程所访问,导致数据被不合理修改。

    判断数据安全:

    • 是否是多线程环境
    • 是否有共享数据
    • 是否有多条语句操作共享数据

    解决数据安全问题:

    • 设计思想:让程序没有安全问题的环境
    • Java提供了同步代码块的解决方式

    锁多条代码块操作共享数据,可以使用同步代码块实现:

    synchronized (任意对象) {
        // 多条语句操作共享数据代码
    }
    

    相当于给代码块内部的代码加锁,任意对象可以看成是一把锁。

    public class SellTicket implements Runnable {
    
        private int tickets = 100;
        private Object obj = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                    }
                }
            }
        }
        
    }
    

    最好的加锁方式就是重新定义一个对象,传输到任意对象的位置;这样当不同线程使用的时候,就会默认变成加了不同的锁,从而实现加锁的目的。

    • 好处:解决了多线程的数据安全问题
    • 弊端:当线程很多的时候,每次线程在运行同步代码块之前都需要判断上锁的状态,这是很耗费资源的,会拖累运行效率

    同步方法

    同步方法就是在方法上添加关键字 synchronized ,加锁的对象是this

    同步方法格式:

    private synchronized void methodName() {}
    

    同步静态方法就是在静态方法上添加关键字 synchronized,加锁的对象是 类名.class

    同步静态方法格式:

    private static synchronized void methodName() {}
    

    线程安全的类

    StringBuffer

    • 线程安全的可变序列。
    • 从JDK 5开始,被StringBuilder替代。通常应该使用StringBuilder,因为它支持所有相同的操作不执行同步,执行速度更快。

    Vector

    • 从Java 2平台v1.2开始,该类改进了List接口。与新的集合实现不同,它实现了同步,这意味着它是线程安全的。如果不需要线程同步,建议使用ArrayList对象。

    Hashtable

    • 该类实现了一个Hash表,他将键映射到值。任何非NULL对象都可以用作键或者值。
    • 从Java 2平台v1.2开始,该类改进了Map接口。与新的集合实现不同,它实现了同步,这意味着它是线程安全的。如果不需要线程同步,建议使用HashMap对象。

    上述对应的线程安全类都有其对应的普通实现类,实例如下:

    public static void main(String[] args) {
        StringBuffer sb1 = new StringBuffer();
        StringBuilder sb2 = new StringBuilder();
    
        Vector<String> vector = new Vector<String>();
        ArrayList<String> arrayList = new ArrayList<String>();
    
        Hashtable<String, String> hashtable = new Hashtable<String, String>();
        HashMap<String, String> hashMap = new HashMap<String, String>();
    
        // synchronizedList(List<T> list) 返回由指定列表支持的同步(线程安全)列表
        List<String> strings = Collections.synchronizedList(new ArrayList<String>());
    }
    
    public static void main(String[] args) {
        StringBuffer sb1 = new StringBuffer();
        StringBuilder sb2 = new StringBuilder();
    
        Vector<String> vector = new Vector<String>();
        ArrayList<String> arrayList = new ArrayList<String>();
    
        Hashtable<String, String> hashtable = new Hashtable<String, String>();
        HashMap<String, String> hashMap = new HashMap<String, String>();
    
        // synchronizedList(List<T> list) 返回由指定列表支持的同步(线程安全)列表
        List<String> strings = Collections.synchronizedList(new ArrayList<String>());
    }
    

    VectorHashtable现在已经不常用了,经常已经被后面的形式所替代。

    Lock锁

    Lock锁提供了比synchronized同步块更为广泛的锁操作,可以实现更多复杂的锁操作。

    Lock中提供了获得锁和释放锁的操作:

    • void lock() 获得锁
    • void unlock() 释放锁

    其中Lock是一个接口,不能直接实例化,我们使用到它的具体实现类ReentrantLock来进行实例化。

    构造方法:

    public ReentrantLock() {}  // 获得一个ReentrantLock的实例
    

    案例:卖票案例,利用Lock锁对象来实现

    public class SellTicket implements Runnable{
        private int tickets = 100;
        // 创建锁的对象
        private Lock lock = new ReentrantLock();
        @Override
        public void run() {
            while (true) {
                try {
                    lock.lock();
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    

    在这个过程中,在lock()unlock()之间的代码就会默认是上锁的代码。作用效果和synchronized是一样的,但是为了防止上锁的代码部分在执行的过程中出现问题,我们将unlock()的调用放到finally代码块中,这样我们才能保证整个程序在运行的过程中不会出现问题。

    生产者消费者问题

    生产者消费者问题,实际上就是两类线程的问题:

    • 一类是生产者线程用于生产数据
    • 一类是消费者线程用于消费数据

    用于解耦生产者和消费者之间的关系,通常会采用一个共享数据的区域,我们通常把它看成是一个仓库

    • 生产者生产数据之后直接放在共享的数据区域中,并不需要关心消费者的行为
    • 消费者只需要从共享区域中获取到共享的数据,并不需要关心生产者的行为

    为了体现生产者和消费者之间的等待和唤醒,Java中提供了几个方法供我们使用:

    image-20210525130816145

    实现案例:

    牛奶生产者和消费者,通过一个存放牛奶的奶箱实现两者的交流

    image-20210525131042096

    开发步骤:

    ①创建生产者类Producer

    public class Producer implements Runnable{
        private Box box;
    
        public Producer(Box box) {
            this.box = box;
        }
    
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                box.put(i);
            }
        }
    }
    

    ②创建消费者类对象Customer

    public class Customer implements Runnable{
        private Box box;
        public Customer(Box box) {
            this.box = box;
        }
    
        @Override
        public void run() {
            while (true) {
                box.get();
            }
        }
    }
    

    ③创建共享数据对象Box

    public class Box {
        // 定义一个成员变量,表示是第几瓶奶
        private int milk;
        // 定义一个成员变量,表示奶箱的状态
        private boolean state = false;
        // 定义一个存储牛奶以及获取牛奶的方法
        public synchronized void put(int milk) {
            // 如果存在牛奶,等待消费
            if (state) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 如果没有牛奶,则生产牛奶
            this.milk = milk;
            System.out.println("送奶工将第" + this.milk + "瓶奶送到");
            // 生产牛奶完毕,修改奶箱状态
            state = true;
            // 唤醒其他等待的线程
            notifyAll();
    
        }
        public synchronized void get() {
            // 如果没有牛奶,等待生产
            if (!state) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 如果存在牛奶,进行消费
            System.out.println("消费者将第" + this.milk + "瓶奶取走");
            // 消费完毕,修改奶箱状态
            state = false;
            // 唤醒其他线程
            notifyAll();
        }
    }
    

    ④创建操作实现类BoxDemo

    public class BoxDemo {
        public static void main(String[] args) {
            // 创建奶箱对象,表示这是共享数据区
            Box box = new Box();
    
            // 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
            Producer p = new Producer(box);
            // 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用取走牛奶的操作
            Customer c = new Customer(box);
    
            // 创建两个线程,分别把生产者和消费者对象作为参数传递
            Thread t1 = new Thread(p);
            Thread t2 = new Thread(c);
            // 启动线程
            t1.start();
            t2.start();
        }
    }
    

    最终的运行结果:

    送奶工将第1瓶奶送到
    消费者将第1瓶奶取走
    送奶工将第2瓶奶送到
    消费者将第2瓶奶取走
    送奶工将第3瓶奶送到
    消费者将第3瓶奶取走
    送奶工将第4瓶奶送到
    消费者将第4瓶奶取走
    送奶工将第5瓶奶送到
    消费者将第5瓶奶取走

    八、网络编程

    概述

    image-20210525135210984

    网络编程

    • 在网络通信协议下,实现网络互连的不同计算机上运行的程序间可以进行数据交换

    网络编程三要素

    image-20210525135553182

    IP地址

    IP地址是网络中设备的唯一标识,IP地址分为两大类

    image-20210525135716104

    常用命令

    • ipconfig 用来查看本机IP地址相关信息
    • ping ip地址 检查网络的连通信

    特殊地址

    127.0.0.1 回送地址,可以代表本机地址,一般用来测试使用

    InetAddress

    为了方便网络编程,Java提供了InetAddress类用来获取IP地址

    InetAddress类表示Internet协议(IP)地址

    image-20210525140730850

    使用案例:

    public static void main(String[] args) throws UnknownHostException {
        // InetAddress address = InetAddress.getByName("DESKTOP-6QQI4OP");
        InetAddress address = InetAddress.getByName("192.168.123.231");
    
        // 获取IP地址的主机名
        String hostName = address.getHostName();
        System.out.println("主机名:" + hostName);
    
        // 获取IP地址
        String ip = address.getHostAddress();
        System.out.println("IP地址:" + ip);
    }
    

    端口

    端口:设备上应用程序的唯一标识

    端口号:用两个字节表示的整数,取值范围值0-65535;其中,0-1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前应用启动失败。

    协议

    协议:计算机网络中,连接和通信的规则被称为网络通信协议

    image-20210525142344571

    image-20210525142538654

    三次握手示意图

    image-20210525142625268

    TCP和UDP

    UDP通信程序

    UDP是一种不可靠的网络传输协议,他在通信两端各建立一个Socket对象,但是这两个Socket只是发送/接收数据的对象;因此对基于UDP通信协议的双方而言,没有所谓的客户端服务器的概念。

    Java提供了DatagramSocket类作为基于UDP协议的Socket

    发送数据的步骤

    ①创建发送端的Socket对象DatagramSocket

    ②创建数据,并把数据打包

    ③调用DatagramSocket对象的方法发送数据

    ④关闭发送端

    public class SendDemo {
        public static void main(String[] args) throws IOException {
            // 创建发送端的 Socket 对象 DatagramSocket
            // DatagramSocket() 构建一个数据报套接字绑定到本地主机的任何可用的端口
            DatagramSocket ds = new DatagramSocket();
    
            // 创建数据,并把数据打包
            // DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
            // 构造一个指定长度的数据包,发送到指定主机上的指定端口号
            byte[] bytes = "Hello, World".getBytes();
            /*int length = bytes.length;
            InetAddress address = InetAddress.getByName("192.168.123.231");
            int port = 10010;
            DatagramPacket dp = new DatagramPacket(bytes, length, address, port);*/
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.123.231"), 10010);
    
            // 调用 DatagramSocket 对象的方法发送数据
            // void send(DatagramPacket p)  从这个套接字发送数据报包
            ds.send(dp);
    
            // 关闭发送端
            // void close() 关闭该数据报套接字
            ds.close();
        }
    }
    

    接收数据的步骤

    ①创建一个接收端的Socket对象用于接收数据(DatagramSocket

    ②创建一个数据包用于接收数据

    ③调用DatagramSocket的方法用于接收数据

    ④解析数据包,并在控制台打印数据

    ⑤关闭接收端

    public class ReceiveDemo {
        public static void main(String[] args) throws IOException {
            // ①创建一个接收端的Socket对象用于接收数据(DatagramSocket)
            DatagramSocket ds = new DatagramSocket(10010);
    
            // ②创建一个数据包用于接收数据
            // DatagramPacket(byte[] buf, int length)  构造一个DatagramPacket用于接收数据包长度为 length 的数据包
            byte[] bytes = new byte[1024]; // 实际数据长度大小可能并没有这么多
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
    
            // ③调用 DatagramSocket 的方法用于接收数据
            ds.receive(dp);
    
            // ④解析数据包,并在控制台打印数据
            // byte[] getData() 返回数据缓冲区
            byte[] data = dp.getData();
            // int getLength()  返回要发送的数据的长度或收到的数据的长度
            int length = dp.getLength();
            String dataStr = new String(data, 0, length);
            System.out.println(dataStr);
    
            // ⑤关闭接收端
            ds.close();
        }
    }
    

    运行时,先运行接收端,接收端会一直开启等待数据发送;之后运行发送端,发送端发送数据由接收端接收之后,接收端会执行相关操作,最后在控制台打印输出相关的数据。

    image-20210525150404039

    练习:UDP通信

    按下面要求实现程序:

    • UDP发送数据:数据来自于键盘输入,直到输入的数据是886,发送数据结束
    • UDP接收数据:数据来自于发送程序,因为不知道什么时候停止接收数据,故采用死循环接收

    发送端程序:

    public class SendDemo {
        public static void main(String[] args) throws IOException {
            // 从键盘录入数据进行发送,直到录入的数据是886,停止录入
            // 创建发送端的Socket对象 DatagramSocket
            DatagramSocket ds = new DatagramSocket();
    
            // 自己封装一个键盘录入
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String line;
            while ((line = br.readLine()) != null) {
                // 判断数据是否是 886
                if ("886".equals(line)) {
                    break;
                }
                // 创建发送端的数据包对象
                byte[] bytes = line.getBytes();
                DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.123.231"), 10086);
    
                // 调用DatagramSocket对象的相关方法进行发送
                ds.send(dp);
            }
    
            // 关闭发送端
            ds.close();
        }
    }
    

    接收端程序:

    public class ReceiveDemo {
        public static void main(String[] args) throws IOException {
            // 创建接收端对象
            DatagramSocket ds = new DatagramSocket(10086);
            while (true) {
                // 调用 DatagramSocket对象的接收方法
                byte[] bytes = new byte[1024];
                DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
                // 接收数据,对数据进行解析
                ds.receive(dp);
                String data = new String(dp.getData(), 0, dp.getLength());
                System.out.println(data);
                // 关闭接收端,死循环接收数据,无操作
            }
        }
    }
    

    TCP通信程序

    TCP通信协议是一个可靠的网络通信协议。它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。
    Java对基于TCP协议的网络通信提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
    Java为客户端提供了Socket类,为服务器端提供了ServerSocket类。

    TCP发送数据

    步骤:

    ①创建客户端的Scoket对象

    ②获取输出流,写数据

    ③释放资源

    public class ClientDemo {
        public static void main(String[] args) throws IOException {
            // 创建Scoket对象
            // Socket(InetAddress address, int port) 创建一个流套接字,并将其与指定的IP地址中的指定端口号连接起来
            // Socket s = new Socket(InetAddress.getByName("192.168.123.231"), 10000);
            // Socket(String host, int port) 创建一个流套接字,并将其与指定的主机上的指定端口号连接起来
            Socket s = new Socket("192.168.123.231", 10000);
    
            // 获取输出流,写数据
            // OutputStream getOutputStream() 返回此套接字的输出流
            OutputStream os = s.getOutputStream();
            os.write("Hello, World!".getBytes());
    
            // 释放资源
            os.close();
            s.close();
        }
    }
    

    TCP接收数据

    步骤:

    ①创建服务器端的Socket对象(ServerSocket

    ②监听客户端连接,并返回Socket对象

    ③获取输入流,读数据,并把数据显示输出在控制台

    ④释放资源

    public class ServerDemo {
        public static void main(String[] args) throws IOException {
            // 创建服务器端的Socket对象(`ServerSocket`)
            ServerSocket ss = new ServerSocket(10010);
    
            // 获取输入流,读数据,并把数据显示输出在控制台
            // Socket accept() 监听要对这个套接字作出的连接并接受它
            Socket s = ss.accept();
            InputStream is = s.getInputStream();
            byte[] bytes = new byte[1024];
            int len = is.read(bytes);
            String data = new String(bytes, 0, len);
            System.out.println(data);
    
            // 释放资源
            s.close();
            ss.close();
    
        }
    }
    

    运行时先运行服务器端程序,之后再运行客户端程序;由服务器端程序监听连接状态,客户端程序发送连接请求,通过TCP协议进行连接通信,之后各自分别进行数据的发送和接收。

    练习:TCP通信

    案例一:需求如下:

    • 客户端:发送数据,接收服务器端反馈
    • 服务器端:接收数据,给出反馈
    public class ServerDemo {
        public static void main(String[] args) throws IOException {
            // 创建ServerSocket对象
            ServerSocket ss = new ServerSocket(10010);
    
            // 监听连接,得到Socket对象
            Socket s = ss.accept();
            InputStream is = s.getInputStream();
            byte[] bytes = new byte[1024];
    
            // 读取数据,释放资源
            int len = is.read(bytes);
            String data = new String(bytes, 0, len);
            System.out.println("服务器端:" + data);
    
            // 给客户端发出反馈
            OutputStream os = s.getOutputStream();
            os.write("数据已成功发送".getBytes(StandardCharsets.UTF_8));
    
            // 释放资源
            ss.close();
        }
    }
    
    public class ClientDemo {
        public static void main(String[] args) throws IOException {
            // 首先创建Socket对象
            Socket s = new Socket("192.168.123.231", 10010);
    
            // 获取输出流,写数据
            OutputStream os = s.getOutputStream();
            os.write("Hello TCP Server".getBytes());
    
            // 接收服务器端的反馈
            InputStream is = s.getInputStream();
            byte[] bytes = new byte[1024];
            int len = is.read(bytes);
            String data = new String(bytes, 0, len);
            System.out.println("客户端:" + data);
    
            // 释放资源
            s.close();
        }
    }
    

    案例二:需求如下所示,要求运用TCP协议

    • 客户端:数据来自于键盘,直到输入的数据的数字是886,输入结束
    • 服务器端:数据来自于客户端,将客户端的数据显示在控制台
    public class ClientInputData {
        public static void main(String[] args) throws IOException {
            // 创建一个 Socket 对象
            Socket s = new Socket("192.168.123.231", 10010);
    
            // 从键盘读取数据,一直到读取到特定字符结束读取
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            // 封装输出流对象
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            String line;
            while ((line = br.readLine()) != null) {
                if ("886".equals(line)) {
                    break;
                }
                // 获取输出流,写数据
                /*OutputStream os = s.getOutputStream();
                os.write(line.getBytes(StandardCharsets.UTF_8));*/
                bw.write(line);
                bw.newLine();
                bw.flush();
            }
    
            // 释放资源
            s.close();
        }
    }
    
    public class ServerInputData {
        public static void main(String[] args) throws IOException {
            // 创建一个 ServerSocket 对象
            ServerSocket ss = new ServerSocket(10010);
    
            // 监听连接,获取 Socket 对象
            Socket s = ss.accept();
            /*InputStream is = s.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);*/
            BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
    
            // 释放资源
            ss.close();
        }
    }
    

    案例三:需求如下:

    • 客户端:数据来自于文本文件,接收服务器反馈
    • 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程

    ①创建线程类

    public class ServerThread implements Runnable {
        private Socket s;
        public ServerThread(Socket s) {
            this.s = s;
        }
    
        @Override
        public void run() {
            // 接收数据写到文本文件
            try {
                BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
                // BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy.java"));
                // 解决名称问题
                int count = 0;
                File file = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy(" + count + ").java");
                while (file.exists()) {
                    count++;
                    file = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy(" + count + ").java");
                }
                BufferedWriter bw = new BufferedWriter(new FileWriter(file));
                String line;
                while ((line = br.readLine()) != null) {
                    bw.write(line);
                    bw.newLine();
                    bw.flush();
                }
    
                // 给出反馈
                BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
                bwServer.write("文件上传成功!");
                bwServer.newLine();
                bwServer.flush();
    
                // 释放资源
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    ②创建服务器类

    public class ThreadServer {
        // 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程
        public static void main(String[] args) throws IOException {
            // 创建服务器 Socket 对象
            ServerSocket ss = new ServerSocket(10010);
    
            // 监听服务器连接,获取Socket对象
            while (true) {
                Socket s = ss.accept();
                new Thread(new ServerThread(s)).start();
            }
    
            // 不需要关闭服务器
        }
    }
    

    ③创建客户端类

    public class ThreadClient {
        public static void main(String[] args) throws IOException {
            // 创建客户端 Socket 对象
            Socket s = new Socket("192.168.123.231", 10010);
    
            // 封装上传的文本文件
            BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\src\\TCP_exercise\\ThreadClient.java"));
            // 封装输出流写数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
    
            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
                bw.flush();
            }
    
            s.shutdownOutput();
    
            // 接收反馈
            BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
            String read = brClient.readLine();
            System.out.println(read);
    
            // 释放资源
            br.close();
            s.close();
        }
    }
    

    九、Lambda表达式

    函数式编程思想概述

    面向对象的思想强调:”必须通过对象的形式来工作“
    函数式编程思想则尽量忽略面向对象的复杂思想;”强调做什么,而不是以什么形式去做“

    我们学习的Lambda表达式就是以函数式编程思想的一种体现,jdk 8 新特性。

    Lambda表达式

    体验

    需求:启动一个线程,在控制台输出一句话:多线程程序启动了

    public class LambdaDemo {
        public static void main(String[] args) {
            // 实现类的方式实现
            /*MyRunnable mr = new MyRunnable();
            Thread t1 = new Thread(mr);
            t1.start();*/
            // 匿名内部类的方式实现
            /*new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("多线程程序启动了");
                }
            }).start();*/
            // Lambda 表达式的方式实现
            new Thread( () -> {
                System.out.println("多线程程序启动了");
            }).start();
        }
    }
    

    我们提供了三种方式实现这种需求,其中Lambda表达式的方式最为简便高效。

    Lambda表达式的标准格式

    image-20210526014306386

    三要素:形式参数、箭头、代码块

    (formal parameters) -> { 
        // code
    }
    
    • 形式参数如果有多个,中间用逗号隔开;如果没有参数,留空即可。
    • ->由英文的中划线加大于号组成,是一种固定写法,代表指向动作
    • 代码块是我们要做的具体内容,也就是我们之前的方法体内容
    • 使用前提:①有一个接口;②接口中有且仅有一个抽象方法

    Lambda表达式练习

    案例一:

    • 定义一个接口Eatable,里面定义一个方法:void eat();
    • 定义一个测试类,在测试类中提供两个方法:
      • 一个方法是useEatable(Eatable e)
      • 另一个方法是主方法,在主方法中调用useEatable方法

    ①创建接口类

    public interface Eatable {
        void eat();
    }
    

    ②创建接口对应的实现类

    public class EatableImpl implements Eatable{
        @Override
        public void eat() {
            System.out.println("一天一苹果,医生远离我");
        }
    }
    

    ③创建测试类,用三种方法实现调用

    public class EatableDemo {
        public static void main(String[] args) {
            Eatable e = new EatableImpl();
            useEatable(e);
    
            // 匿名内部类
            useEatable(new Eatable() {
                @Override
                public void eat() {
                    System.out.println("一天一苹果,医生远离我");
                }
            });
    
            // Lambda 表达式的使用
            useEatable(() -> {
                System.out.println("一天一苹果,医生远离我");
            });
        }
    
        private static void useEatable(Eatable e) {
            e.eat();
        }
    }
    

    案例二:

    • 定义一个接口Flyable,里面定义一个方法:void fly(String s);
    • 定义一个测试类,在测试类中提供两个方法:
      • 一个方法是useFlyable(Flyable f)
      • 另一个方法是主方法,在主方法中调用useFlyable方法

    ①定义一个接口

    public interface Flyable {
        void fly(String s);
    }
    

    ②创建测试类

    public class FlyableDemo {
        public static void main(String[] args) {
            // 匿名内部类实现
            useFlyable(new Flyable() {
                @Override
                public void fly(String s) {
                    System.out.println(s);
                    System.out.println("匿名内部类");
                }
            });
    
            // Lambda 表达式
            useFlyable( (String s) -> {
                System.out.println(s);
                System.out.println("Lambda表达式");
            });
        }
        private static void useFlyable(Flyable f) {
            f.fly("风和日丽,晴空万里");
        }
    }
    

    案例三:

    • 定义一个接口Addable,里面定义一个方法:int add(int x,int y);
    • 定义一个测试类,在测试类中提供两个方法:
      • 一个方法是useAddable(Addable a)
      • 另一个方法是主方法,在主方法中调用useAddable方法

    ①创建接口

    public interface Addable {
        int add(int x, int y);
    }
    

    ②创建测试类

    public class AddableDemo {
        public static void main(String[] args) {
            // 匿名内部类实现
            useAddable(new Addable() {
                @Override
                public int add(int x, int y) {
                    return x + y;
                }
            });
    
            // Lambda 表达式
            useAddable( (int x, int y) -> {
                return x + y;
            });
        }
        private static void useAddable(Addable a) {
            int sum = a.add(10, 20);
            System.out.println(sum);
        }
    }
    

    Lambda表达式的省略模式

    省略模式:

    • 参数类型可以省略,但是有多个参数的时候,不能只省略部分
    • 如果参数有且仅有一个,那么小括号可以省略
    • 如果代码块的语句只有一条,可以省略大括号和分号,甚至是return

    调用之前编写的接口,尝试Lambda表达式的省略模式的运用:

    public class LambdaDemo {
        public static void main(String[] args) {
            useAddable((int x, int y) -> {
                return x + y;
            });
            // 省略模式:参数的类型可以省略
            // 但是有多个参数的情况下,不能只省略部分
            useAddable((x, y) -> {
                return x + y;
            });
            useFlyable((s) -> {
                System.out.println(s);
            });
            // 省略模式:如果参数仅有一个,小括号()可以省略
            useFlyable(s -> {
                System.out.println(s);
            });
            // 省略模式:如果代码块的语句只有一条,可以省略大括号{}和代码块语句的分号;
            useFlyable(s -> System.out.println(s));
            // 省略模式:如果代码块的语句只有一条,可以省略大括号{}和代码块语句的分号; 如果有 return 语句 ,return 也要省略掉
            useAddable((x, y) -> x + y);
        }
    
        private static void useAddable(Addable addable) {
            int sum = addable.add(10, 20);
            System.out.println(sum);
        }
    
        private static void useFlyable(Flyable flyable) {
            flyable.fly("风和日丽,晴空万里");
        }
    }
    

    Lambda表达式的注意事项

    注意事项:

    • 使用Lambda必须要有一个接口,且接口中有且仅有一个抽象方法

    • 使用Lambda必须要有上下文环境,才能推导出Lambda对应的接口

      • // 根据局部变量的赋值得知
        Runnable r = () -> System.out.println("Lambda表达式");
        new Thread(r).start();
        
      • // 根据调用方法的参数得知
        new Thread(() -> System.out.println("Lambda表达式")).start();java
        

    测试类:

    public class LambdaDemo {
        public static void main(String[] args) {
            // 注意:使用Lambda必须要有一个接口,且接口中有且仅有一个抽象方法
            useInter(() -> System.out.println("好好学习,天天向上"));
            // 注意:使用Lambda必须要有上下文环境,才能推导出Lambda对应的接口
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("匿名内部类");
                }
            }).start();
            // 根据局部变量的赋值得知
            Runnable r = () -> System.out.println("Lambda表达式");
            new Thread(r).start();
            // 根据调用方法的参数得知
            new Thread(() -> System.out.println("Lambda表达式")).start();
    
        }
        private static void useInter(Inter i) {
            i.show();
        }
    }
    

    Lambda表达式和匿名内部类的区别

    • 所需类型不同
      • 匿名内部类:可以是接口,可以是抽象类,也可以是具体类
      • Lambda表达式:只能是接口,而且接口中只能有一个抽象方法
    • 使用限制不同
      • 如果一个接口中有且仅有一个抽象方法,可以使用匿名内部类,也可以使用Lambda表达式
      • 如果一个接口中存在多个抽象方法,只能使用匿名内部类
    • 实现原理不同
      • 匿名内部类:编译之后,会在磁盘中产生一个单独的.class字节码文件
      • Lambda表达式:编译之后,没有一个单独的.class字节码文件,对应的字节码会在运行的时候动态生成

    匿名内部类在编译之后会在内存中产生一个单独的.class字节码文件

    public class LambdaDemo {
        public static void main(String[] args) {
            // 匿名内部类
            /*useInter(new Inter() {
                @Override
                public void show() {
                    System.out.println("接口");
                }
            });
            useAnimal(new Animal() {
                @Override
                void method() {
                    System.out.println("抽象类");
                }
            });
            useStudent(new Student() {
                @Override
                void study() {
                    System.out.println("具体类");
                }
            });*/
    
            // Lambda
            useInter(() -> System.out.println("接口"));
            // Lambda只支持接口类型,且接口中仅有一个抽象方法
            /*useAnimal(() -> System.out.println("抽象类"));
            useStudent(() -> System.out.println("具体类"));*/
        }
        private static void useInter(Inter i) {
            i.show();
        }
        private static void useAnimal(Animal a) {
            a.method();
        }
        private static void useStudent(Student s) {
            s.study();
        }
    }
    

    十、反射

    类加载

    当程序要使用某个类时,如果该类还没有被加载到内存中时,该系统会通过类的加载、类的连接、类的初始化三个步骤来对类进行初始化。如果没有出现意外,JVM会连续完成这三个步骤,所以有时也将这三个步骤统称为类加载或类初始化。

    类的加载

    • 类加载就是将class文件读入内存,并为之创建一个java.lang.Class的对象
    • 任何类被使用时,系统都会为之建立一个Java.lang.Class对象

    类的连接

    • 验证阶段:用于检验被加载的类是否具有正确的内部结构,和其他类是否协调一致
    • 准备阶段:负责为类的类变量分配内存,并设置默认初始化值
    • 解析阶段:将类的二进制数据中的符号引用替换为直接引用

    类的初始化

    • 在该阶段,主要就是对类变量进行初始化

    类的初始化步骤

    • 假如该类还没有被加载和连接,则程序先加载并连接该类
    • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
    • 假如类中有初始化语句,则系统依次执行这些初始化语句

    注意:在执行第二个步骤的时候,如果该类还有直接父类,则也依次按照上述步骤执行
    所以,根据这个逻辑,最先被初始化完成的是java.lang.Object类

    类的初始化时机

    • 创建类的实例
    • 调用类的类方法
    • 访问类或者类接口的类变量,或者为该类变量赋值
    • 使用反射方式来强制创建某个类或者接口对应的java.lang.Class对象
    • 初始化某个类的子类
    • 直接使用java.exe命令来运行某个主类

    类加载器

    作用:

    • 负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象
    • 虽然不用过分关注类加载机制,但是了解类加载机制我们能够更好地了解整个程序的运行

    JVM的类加载机制

    • 全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    • 父类委托:就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    • 缓存机制:保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

    ClassLoader:负责加载类的对象

    Java运行时有以下内置的加载器

    • Bootstrap class loader:它是虚拟机的内置类加载器,通常表示为null,并且没有父null
    • Platform class loader:平台类加载器可以看到所有平台类,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类
    • System class loader:它也被称为应用程序类加载器,与平台类加载器不同。系统类加载器通常用于定义应用程序类路径,模块路径和JDK特定工具上的类
    • 类加载器的继承关系:System的父加载器为Platform,而Platform的父加载器为Bootstrap

    ClassLoader中的两个方法:

    • static ClassLoader getSystemClassLoader():返回用于委派的系统类加载器
    • ClassLoader getParent():返回父类加载器进行委派

    案例代码:

    public class ClassLoaderDemo {
        public static void main(String[] args) {
            // static ClassLoader getSystemClassLoader() :返回用于委派的系统类加载器
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
            System.out.println(systemClassLoader);
            // ClassLoader getParent() :返回父类加载器进行委派
            ClassLoader parent1 = systemClassLoader.getParent();
            // jdk.internal.loader.ClassLoaders$PlatformClassLoader@10f87f48
            System.out.println(parent1);
            ClassLoader parent2 = parent1.getParent();
            // null Bootstrap类加载器,只是因为表示为null
            System.out.println(parent2);
        }
    }
    

    反射

    反射概述

    Java反射机制:是指在运行时去获取一个类的变量和方法信息。然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展

    image-20210528134700143

    获取Class类的对象

    我们要想通过反射去使用一个类,首先我们要获取到该类的字节码文件对象,也就是类型为Class类型的对象

    这里我们提供三种方式获取Class类型的对象

    • 使用类的class属性来获取该类对应的Class对象。举例:Student.class将会返回Student类对应的Class对象
    • 调用对象的getClass()方法,返回该对象所属类对应的Class对象
      • 该方法是Object类中的方法,所有的Java对象都可以调用该方法
    • 使用Class类中的静态方法forName(String className),该方法需要传入字符串参数,该字符串参数的值是某个类的全路径,也就是完整包名的路径

    案例:

    一、首先创建Student类

    public class Student {
        // 提供三个变量:一个私有,一个默认,一个公共
        private String name;
        int age;
        public String address;
        // 提供两个构造方法:一个私有,一个默认,两个公共
        public Student() {
    
        }
        private Student(String name) {
            this.name = name;
        }
        Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public Student(String name, int age, String address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
        // 成员方法:一个私有,四个公共
        private void function() {
            System.out.println("function");
        }
        public void method1() {
            System.out.println("method");
        }
        public void method2(String s) {
            System.out.println("method" + s);
        }
        public String method3(String s, int i) {
            return s + "," + i;
        }
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", address='" + address + '\'' +
                    '}';
        }
    }
    

    二、创建测试类

    public class ReflectDemo {
        public static void main(String[] args) throws ClassNotFoundException {
            // 使用类的class属性来获取该类对应的Class对象
            Class<Student> c1 = Student.class;
            System.out.println(c1);
            Class<Student> c2 = Student.class;
            System.out.println(c1 == c2);
            // 调用对象的 getClass() 方法
            Student s = new Student();
            Class<? extends Student> c3 = s.getClass();
            System.out.println(c3 == c1);
            // 使用Class类中的静态方法 forName(String className)
            Class<?> c4 = Class.forName("get_class.Student");
            System.out.println(c4 == c1);
        }
    }
    

    运行结果:

    class get_class.Student
    true
    true
    true

    反射获取构造方法并调用

    Class类中用于获取构造方法的方法

    • Constructor<?>[] getConstructors():返回所有公共构造方法对象的数组
    • Constructor<?>[] getDeclaredConstructors():返回所有构造方法对象的数组
    • Constructor<T> getConstructor(Class <?> ... parameterTypes):返回单个公共构造方法对象
    • Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes):返回单个构造方法对象

    Constructor类中用于创建对象的方法

    • T newInstance(Object... initargs):根据指定的构造方法创建对象

    案例:利用上面创建的Student类进行测试

    public class ReflectDemo {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 使用静态方法获取上次使用的学生类
            Class<?> aClass = Class.forName("get_class.Student");
            // Constructor<?>[] getConstructors() :返回所有公共构造方法对象的数组
            // Constructor<?>[] constructors = aClass.getConstructors();
            // Constructor<?>[] getDeclaredConstructors() :返回所有构造方法对象的数组
            Constructor<?>[] constructors = aClass.getDeclaredConstructors();
            for (Constructor<?> constructor : constructors) {
                System.out.println(constructor);
            }
            // Constructor<T> getConstructor(Class <?> ... parameterTypes) :返回单个公共构造方法对象
            // Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes) :返回单个构造方法对象
            // 参数:你要获取的构造方法的参数的个数和数据类型对应的字节码文件
            Constructor<?> constructor = aClass.getConstructor();
            // Constructor 提供了一个类的单个函数构造函数的信息和访问权限
            // T newInstance(Object... initargs) :根据指定的构造方法创建对象
            Object obj = constructor.newInstance();
            System.out.println(obj);
        }
    }
    

    练习:使用反射获取构造方法并调用

    练习1:通过反射实现如下操作

    • Student s = new Student("张三", 20, "西安");
    • System.out.println(s);
    • 基本数据类型也可以通过.class得到对应的Class类型
    public class ReflectDemo1 {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 获取 Class 对象
            Class<?> stuClass = Class.forName("get_class.Student");
            // Constructor<T> getConstructor(Class <?> ... parameterTypes) :返回单个公共构造方法对象
            // public Student(String name, int age, String address)
            Constructor<?> constructor = stuClass.getConstructor(String.class, int.class, String.class);
            // 基本数据类型也可以通过.class获取到 Class 类型
            Object obj = constructor.newInstance("张三", 20, "西安");
            System.out.println(obj);
        }
    }
    

    练习2:通过反射实现如下操作

    • Student s = new Student("张三");
    • System.out.println(s);
    • public void setAccessible(boolean flag)值为true,取消访问检查
    public class ReflectDemo2 {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 获取 Class 对象
            Class<?> stuClass = Class.forName("get_class.Student");
            // Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes) :返回单个构造方法对象
            // private Student(String name)
            Constructor<?> constructor = stuClass.getDeclaredConstructor(String.class);
            // java.lang.IllegalAccessException
            // 采用暴力反射,运行 setAccessible 方法,设置为 true 抑制访问检查
            constructor.setAccessible(true);
            Object obj = constructor.newInstance("张三");
            System.out.println(obj);
        }
    }
    

    反射获取成员变量并使用

    Class类中用于获取成员变量的方法:

    • Field[] getFields():返回所有公共成员变量对象的数组、
    • Field[] getDeclaredFields():返回所有成员变量对象的数组
    • Field getField(String name):返回单个公共成员变量对象
    • Field getDeclaredField(String name):返回单个成员变量对象

    Field类中用于给成员变量赋值的方法

    • void set(Object obj, Object value):给obj对象的成员变量赋值为value

    案例:

    public class ReflectDemo {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 获取对象的 Class 类型
            Class<?> aClass = Class.forName("get_class.Student");
            // Field[] getFields():返回所有公共成员变量对象的数组
            // Field[] fields = aClass.getFields();
            // Field[] getDeclaredFields():返回所有成员变量对象的数组
            Field[] fields = aClass.getDeclaredFields();
            for (Field field : fields) {
                System.out.println(field);
            }
            // Field getField(String name):返回单个公共成员变量对象
            // Field getDeclaredField(String name):返回单个成员变量对象
            Field addressField = aClass.getField("address");
            // 获取无参构造创建对象
            Constructor<?> constructor = aClass.getConstructor();
            Object obj = constructor.newInstance();
            // void set(Object obj, Object value):给obj对象的成员变量赋值为value
            addressField.set(obj, "西安");
            System.out.println(obj);
        }
    }
    

    练习:反射获取成员变量并使用

    练习:通过反射实现如下操作

    Student s = new Student();
    s.name = "name" ;
    s.age = 20;
    s.address ="address";
    System.out.println(s);
    

    实现代码:

    public class ReflectDemo {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
            // 获得 Class 对象
            Class<?> stuClass = Class.forName("get_class.Student");
            // 获取无参构造方法创建对象
            Constructor<?> constructor = stuClass.getConstructor();
            Object obj = constructor.newInstance();
            System.out.println(obj);
            // 获取 field 对象、破坏访问权限检查、赋值、输出
            Field nameField = stuClass.getDeclaredField("name");
            nameField.setAccessible(true);
            nameField.set(obj, "雨下一整晚Real");
            System.out.println(obj);
            Field ageField = stuClass.getDeclaredField("age");
            ageField.setAccessible(true);
            ageField.set(obj, 20);
            System.out.println(obj);
            Field addressField = stuClass.getDeclaredField("address");
            addressField.setAccessible(true);
            addressField.set(obj, "Address");
            System.out.println(obj);
        }
    }
    

    反射获取成员方法并使用

    Class类中用于获取成员方法的方法

    • Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
    • Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
    • Method getMethod(String name, Class<?> ... parameterTypes):返回单个公共成员方法对象
    • Method getDeclaredMethod(String name, Class <?> ... parameterTypes):返回单个成员方法对象

    Method类中用于调用成员方法的方法

    • Object invoke(Object obj, Object... args):调用obj对象的成员方法,参数是args,返回值是Object类型

    案例:

    public class ReflectDemo {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            // 获取 Class 对象
            Class<?> stuClass = Class.forName("get_class.Student");
            // Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
            Method[] methods = stuClass.getMethods();
            for (Method method : methods) {
                System.out.println(method);
            }
            // Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
            Method[] declaredMethods = stuClass.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                System.out.println(declaredMethod);
            }
            // Method getMethod(String name, Class<?> ... parameterTypes):返回单个公共成员方法对象
            // Method getDeclaredMethod(String name, Class <?> ... parameterTypes):返回单个成员方法对象
            // public void method1()
            Method method = stuClass.getMethod("method1");
    
            // 获取无参构造方法创建对象
            Constructor<?> constructor = stuClass.getConstructor();
            Object obj = constructor.newInstance();
            // Object invoke(Object obj, Object... args):调用obj对象的成员方法,参数是args,返回值是Object类型
            method.invoke(obj);
        }
    }
    

    练习:反射获取成员方法并调用

    练习:通过反射实现如下操作

    Students = new Student);
    s.method1();
    s.method2("张三" );
    String ss = s.method3("张三",30);
    System.out.printIn(ss);
    s.function();
    
    public class ReflectDemo {
        public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
            // 获取 Class 类型
            Class<?> stuClass = Class.forName("get_class.Student");
            // 获取无参构造方法创建对象
            Constructor<?> constructor = stuClass.getConstructor();
            Object obj = constructor.newInstance();
            // 通过对象调用 method1 方法
            Method method1 = stuClass.getMethod("method1");
            method1.invoke(obj);
            // 调用 method2 方法
            Method method2 = stuClass.getMethod("method2", String.class);
            method2.invoke(obj, "雨下一整晚Real");
            // 调用 method3 方法,存在返回值
            Method method3 = stuClass.getMethod("method3", String.class, int.class);
            Object o = method3.invoke(obj, "雨下一整晚Real", 20);
            System.out.println(o);
            // 调用 function 方法,私有方法
            // Method function = stuClass.getMethod("function"); java.lang.NoSuchMethodException
            Method function = stuClass.getDeclaredMethod("function");
            // 暴力反射,破坏方法调用前的权限检查
            function.setAccessible(true);
            function.invoke(obj);
        }
    }
    

    反射练习

    练习1:有一个ArrayList<Integer>集合,现在要在这个集合中添加一个字符串数据,如何实现?

    public class ArrayListAddString {
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            // 创建集合
            ArrayList<Integer> arrayList = new ArrayList<Integer>();
            /*arrayList.add(10);
            arrayList.add(20);
            arrayList.add("Hello");*/
    
            // 获取 Class 类型
            Class<? extends ArrayList> arrayClass = arrayList.getClass();
            Method add = arrayClass.getMethod("add", Object.class);
            // 利用反射越过泛型检查,调用原始方法的参数类型
            add.invoke(arrayList, "Hello");
            add.invoke(arrayList, "World");
            System.out.println(arrayList);
        }
    }
    

    练习2:通过配置文件运行类中的方法

    public class ConfigurationFileRunMethodInClass {
        public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
            /*
            * Class.txt
            * className=Xxx
            * methodName=Xxx
            * */
            // 加载配置文件
            Properties properties = new Properties();
            FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day16_Reflection\\src\\reflect_exercise\\Class.txt");
            properties.load(fr);
            fr.close();
            // className=reflect_exercise.Student
            // methodName=study
            String className = properties.getProperty("className");
            String methodName = properties.getProperty("methodName");
            // 通过调用反射来实现方法调用
            Class<?> stuClass = Class.forName(className);
            // 通过获取无参构造方法获取对象
            Constructor<?> constructor = stuClass.getConstructor();
            Object obj = constructor.newInstance();
            Method method = stuClass.getDeclaredMethod(methodName);
            method.invoke(obj);
        }
    }
    
    展开全文
  • 从程序员到项目经理

    千次阅读 2016-01-15 17:53:17
    从程序员到项目经理(一):为什么要当项目经理 “从程序员到项目经理”,这个标题让我想起了很久以前一本书的名字《从...而从程序员到项目经理,却并无捷径可走,必须从内而外的改变提升。 一.为什么要当项目经理

    从程序员到项目经理(一):为什么要当项目经理
    “从程序员到项目经理”,这个标题让我想起了很久以前一本书的名字《从Javascript到Java》。然而,从Javascript到Java充其量只是工具的更新,而从程序员到项目经理,却是一个脱胎换骨的过程。从Javascript到Java,是一个取巧的方法;而从程序员到项目经理,却并无捷径可走,必须从内而外的改变和提升。

    一.为什么要当项目经理

    1. 问题本质

    如果我对一个老程序员说:“有必要转项目经理啦”,很多人第一反应是“为什么一定要当项目经理?!”,反问很给力,基至会让人哑口无言。但反问成功的结果可能只是使自己麻醉,暂时忘却现实中面临的烦恼和压力,这无异于把头埋进沙子中的鸵鸟。只有理智的分析,才能作为自己行动的指南。

    首先申明,不是每个程序员都需要当项目经理,也不是每个程序员都想当项目经理,更不是每个程序员都能当项目经理。因此,当不当项目经理,可以说是一个“需不需要、想不想、能不能”的问题。

    想不想,是一个意愿的问题。这是前提,毕竟强扭的瓜不甜嘛。显然,富二代一般是不想当项目经理的,因为他们想直接当总裁。还有些人,只想钻研技术,不想钻研人,他们也是不会想当项目经理的。如果你没有意愿当项目经理,也就没有讨论的必要了。什么,你不知道想不想?呃,那就继续往下读吧,也许读着读着,你就想当了。

    能不能,是能力的问题。这是不关键,因为只要有意愿,能力是可以培养的。程序员连复杂得让人琢磨不透的软件都能搞定,还有什么搞不定的?

    因此最后落实在需不需要这个问题上。这个问题很棘手,需要从程序员自身以及外部环境等方面进行分析。要讨论这个问题,就要弄清楚它和想不想的关系。想和需要是紧密相关的,但并不是一回事。想不想,主是感情的因素,而需不需要则要进行理智的分析的了。理智与感情,并不总是一致的。有些东西,是你需要的,但你未必想要。比如,被困沙漠的时候,有时被逼喝自己的尿液,这是理智战胜了感情。电影《色戒》中的汤唯,则是感情战胜了理智,爱上了敌人,最后造成了悲剧的结局。因此,我们还是少说气话了,不要冲动,冷静的分析自己的处境吧。

    2. 鸭梨山大

    当我从网上看到码农这个词时,觉得网民很有自嘲精神,后来我看到了码畜和码奴这个两个词,不禁从心底涌起了深深的悲哀,为这个行业,也为这个社会。

    看看智慧的网民对IT人士级别的划分:

    IT领袖:年入过亿(例如任正非、马化腾、李彦宏、丁磊、马云等,包括期权股票以及投资理财等收入。)IT大哥:年入千万(级别次于以上几位大佬的公司老板,不缺钱,普遍对上一条里的人物羡慕嫉妒恨。)IT精英:年入百万(各IT公司副总裁级别人物,包括COO、CTO等,大多为职业经理人,赚够钱就跑。)IT人才:年入50万(各IT公司总监级别人物,有房有车,生活压力相对较小)IT工程师:年入20万(高级经理级别,有房贷,生活压力大)IT民工:年入10万(经理级别,基本无房,学会装波一,生活压力大)码农:年入6万到10万(工作三四年,租房,继续混日子)码奴:年入3万到6万(工作一两年,租房,混日子)码畜:年入低于3万(刚毕业的,租房,傻乐)

    我知道你想问什么问题了。不要问哥赚多少,哥只是一个普通的IT人士而已。前面三级都是牛人,是成功人士,他们的作用不是让去成为他们,而是激励我们自己。你现在读到的也不是一篇成功学的文章,而是和你一起分析程序员的处境、以及怎样缓解压力的文章罢了。

    言归正传。看到这个表,是不是有鸭梨山大的感觉。找到了自己的位置吗?什么,不好意思?没关系啦,园子里面不是很多人称自己为程序猿或者猴子吗?那大概也就是相当于码畜吧。我想能读到这篇文章的,大概都是“IT工程师(高级经理)”以下,他们的主要特征是“生活压力大”和“混日子”。如是你是前面四级,建议你果断退出本文。

    我在上一篇博文中提到30岁现象,有些人认为车到山前必有路,这是杞人忧天。不错,程序员确实可以干到30多岁,甚至四五十岁,但他们面临的压力却可能是“不足与外人道也”。

    我经常与30岁以上的程序员交流,他们流露出来的对现状的不满、无奈、无力、对安全感的缺乏,让我感同身受。

    虽然谈压力并不是一件愉快的事情,但我仍然必须要说出来,因为我宁可清醒的痛着,也不要在麻醉中睡去。那就让我们拿着手术刀,对自己进行痛苦的解剖吧。

    下面是一个简单的“危机评估表”,总共有30项。在“是否认同”后面打出分数,每一项如果认同为1分,不认同为0分。

    类别

    评估项

    是否认同

    身体

    悄然发现已经没有以前经折腾了。

     

    没有定期的体育运动。

     

    中餐午餐都是在外面吃快餐。

     

    确信自己是亚健康。

     

    家庭

    每月开支不算不知道,一算吓一跳。

     

    有房贷或房租。

     

    有孩子了,上幼儿园是一笔大开支。或者超过30岁了还没结婚。

     

    买不起车,或有车子,开不起。

     

    家里时有摩擦,经常有不开心的事。

     

    每个月存不了多少钱。

     

    时间

    要花很多时间陪家庭成员。

     

    加班时间越来越少。

     

    社交时间较少。

     

    激情

    只想休息,不想工作。

     

    对新技术、新工具不甚了解,有心无力。

     

    没有制度明确的短期、中期和长期目标。

     

    理想已经模糊了。

     

    社会

    只有交税,没有回报。

     

    担心老了病无所依,老无所养。

     

    担心国家经济衰退,陷入失业。

     

    收入增长跟不上通货膨胀的速度。

     

    行业

    新人比我更具有性价比。

     

    行业竞争激烈,低价抢标现象严重。

     

    行业被某些公司垄断。

     

    行业正在慢慢衰落。

     

    公司

    公司发展前景不是很明朗。

     

    公司薪资福利一般。

     

    公司没有企业文化。

     

    公司员工关系比较紧张,有内斗现象。

     

    公司缺乏活力。

     

    总分

     

    (说明:此表并不精确,仅供参考)

    如果总分小于10分,那要恭喜你,说明你生活稳定幸福,让人羡慕。我觉得这篇文章你也不用往下看了。

    如果你的总分大于20分,说明你承受的压力过大,可能面临职业方面的危机,应当寻求改变了。

    如果总分在10-20分,说明你生活比较稳定,收入方面可能是中上等水平,但职业发展方面仍有风险。

    3. 另一片天地

    所谓“穷则变、变则通”,如果你还是普通的老程序员,并且还在为自己的职业彷徨和苦闷,那就应该寻求变化之道了。

    如果你愿意,转向项目管理乃是上上之策。

    当然转项目管理只是程序员很多选择中的一个。显然不是每个程序员都需要当项目经理。一般每个公司都最少提供了技术和管理两条职业发展通道,如果你技术超牛,你完全可以从程序员做到系统分析师,一直做到技术总监。如果技术方面你信心不足,转项目管理就是一件自然而然的事情了。

    技术和管理,这是两条绝然不同的路,虽然“条条大路通罗马”,但沿途的风景却是完全不一样。一旦你从事了项目管理,你将看到不同的另一片天地。

    (1)在管理的天地里,你将不再有职业瓶颈。

    程序员虽然也可以干一辈子,但工资水平是有天花板的,不要问我为什么,行业就是这样。项目经理则有无限上升的空间,不但工资更高,职位上也可以升至部门经理、副总经理甚至总经理职位。

    (2)促进项目经理内在成长,心智更加成熟。

    美国项目管理协会PMI认为,项目经理75%-90%的时间应该用在沟通上。沟通的对象显然是人,因此,项目管理主是要一项与人打交道的工作。如果说解决技术问题人主要是靠一个人的智商,那么与人打交道,则是要靠一个人的情商。

    虽然不当项目经理也可以发展情商,但在项目中锻炼是自我成长、自我完善的捷径。

    (3)项目管理知识可以用在生活中的各个方面。

    生活中的许多事情,我们并没有称之为一个项目,但可以用项目管理的方法来对待。例如一次婚礼的组织,或一次自助旅游。你在项目管理中培养起来的情商,更是让你面对生活中的各种问题游刃有余,你的家庭也会更家和谐,就像范范的一首歌里唱的:“好像什么困境都知道该怎么办”。当到达这种境界时,你会有一种海阔天高,一览众山小的感觉。

    因此,即使你不想从事项目管理,也建议你学习一下项目管理知识。有一本书叫《不懂项目管理,还敢拼职场》,虽然觉得内容一般,但对标题深以为然。

    二.项目管理倒底难不难

    程序员问:“我现在想当项目经理,但心里没底,不知道项目管理到底难不难?”这个问题确实不好回答。俗话说,“会者不难、难者不会”,很多事情都是如此。

    有些人觉得不难,他们好像天生就具有管理的才能,他们举止得体、八面玲珑,具有很强的个人魅力,可以把大事化成小事,把坏事变成好事。这样的人,想不成功都难。

    大部分人还是会觉得难。在PMI的知识体系里,项目管理有九大领域,五大过程组,44个过程,有数不清的工具和方法。项目执行中方方面面出了问题,都是项目经理的责任,项目经理又不是超人,怎么应付得过来。项目管理确实有点难。

    你若问我,我会说项目管理既难,又不难。对于愿意改变自己的人而言,它不难;对于性格偏执的人而言,项目管理确实太难了。

    很多人无法意识到自己的偏执。上级只要提出一点批评,他们就要拼命的辩解和反驳。他们的保护壳太厚了。

    项目经理最重要的素质,就是心智的成熟,一个心智成熟的人,不会是一个偏执的人。

    毕竟,人无完人,项目经理必须从善如流,才能完成自己角色的转变。对于从程序员转过来的项目经理,做事的方法与以前应是翻天覆地的不同,必须迅速审时夺势,改变自己。否则,那你不还只是个有项目经理职位的程序员么?

    因此可以说,项目管理难就难在项目经理要改变自己。这个改变,不只是知识体系的扩充,更可能是性格的改变,而一个人要改变性格是极其困难的。

    程序员习惯于与机器打交道,通过严密的代码和逻辑来控制机器;而项目经理是跟人打交道,人是有感情的,绝对不是你给他输入1+1,他就给你输出2。项目经理必须时时用心去思考、体会,然后改进。几番回合下来,项目经理会惊喜的发现自己变了,有种脱胎换骨的感觉—-那是当然的,因为变得更成熟了。

    只要你愿意改变自己,假以时日,你一定会成为一个优秀的项目经理。

    三.程序员应克服的障碍

    程序员与项目经理之间,往往有一条鸿沟。对技术钻研越深的程序员,这条鸿沟可能越大。这是由程序员的性格特征决定的。

    程序员普遍有非常多的优点:例如聪明、逻辑思维强、学习能力强、创新能力强、直率等。但优点往往也是弱点之所在,例如:

    (1)太讲逻辑:与人相处时容易忽视人际关系、感情等方面的因素。

    (2)过于直率:说话直来直去,容易伤害他人感情。

    (3)自傲:总觉得自己技术不错、比周围的人要强一点。好比一只鸡看到同类觉得自己最大,看到鹅觉得跟自己差不多,看到火鸡才觉得比自己大一点。

    (4)固执:在自己的逻辑中不能自拔,无法听取别人的意见。

    (5)沟通能力较弱:大部分程序员在口头表达、写作、汇报、交流等方面存在不足。

    而这些缺点,也是心智不够成熟有表现,这是项目经理的大忌,往往会成为程序员晋升项目经理的障碍。因此,必须要克服这些障碍,给自己制定符合项目经理要求的行为准则,时时提醒自己,每日进行反省,坚持下去,必然会成功。

    从程序员到项目经理(二):升职之辨
    被任命为项目经理,是职业生涯的第一次飞跃,既惊喜又紧张。从现在开始,你要思考怎样才能胜任项目管理的工作,否则等着你的,很可能是一场悲剧。

    一.升职之辨

    1. 为什么是我

    不是每个人都能当项目经理,程序员中只有一小部分能成为项目经理,大部分人会随着岁月的流逝,成为了“资深程序员”。

    那为什么领导要选择我呢?一般人对自己所拥有的东西都会很快习以为常,认为这是自己应得的。一点也没错,这就是你应得的,原因也很简单,那是因为你比别人优秀一点。

    其实领导挑选人才的标准很简单,那就是你比别人优秀,而且只需一点点。你不需要“鹤立鸡群”,“鸭立鸡群”已经足够了。俗话说:“群众的眼睛是雪亮的”,其实领导眼睛才是真正雪亮的,如果他还没有发现你,那是因为你还不够优秀,没有引起他的注意。

    因此,如果你工作多年仍然没有职位上升,不要埋怨公司不给你机会,而应该从自己身上找原因,机会只会给有准备的人。如果你不知道自己准备好了没有,就试着回答下面的问题吧:

    ● 工作是不是比别人积极主动一点;

    ● 加班是不是比别人多一点(如果贵公司喜欢员工加班的话);

    ● 提交成果是不是比别人提前一点;

    ● 成果质量是不是比别人要好一点;

    ● 学习是不是比别人勤奋一点;

    ● 面对问题是不是比别人勇敢和执着一点;

    ● 人际关系是不是更和谐一点。

    如果你能做到这些,相信机会迟早会属于你的。

    2. 彼得定律的启发

    心理学中有一个词,叫“光环效应”,是说当我们对一个人某个方面有好的印象时,我们会倾向于认为他的其他方面也是好的。因此,当你能胜任你现有职位、比别人优秀一点时,领导会认为你是下一个职位的最佳人选。然而实际上,你不一定是最合适的,但有什么关系呢,你已经是项目经理了,你有很多时间,可以边做边学。但是,如是你长期不胜任项目管理工作,项目经理就会成为你职业生涯的最高职位。

    这也就是彼得定律的内涵:“在一个等级制度中,每个员工趋向于上升到他所不能胜任的职位”。

    从彼得定律中,我们可以得到以下启发:

    (1)在公司里面,大部分人都干着他不能胜任的事情。这听起来真是一个悲剧,好在我们暂时还不用操心。

    (2)金子是一定会发光的,人才绝对不会被埋没的。这是由于人才的稀缺性造成的,只要是胜任当前职位,晋升是迟早的事。因此,无论是程序员还是项目经理,都要做好你的本职工作,这才是最重要的。试想,如果本职工作都没做好,怎么可能提拔到更高职位呢?别告诉我还可以走后门。

    (3)当上了项目经理,只是说明你可以胜任程序员职位,而不意味着你可以胜任项目经理。因此,别急着庆祝,还是多想想怎么来管项目的事情吧,否则你就可能是下一场悲剧的主角。

    (4)如果你已经担任项目经理很长时间,还没有得到升迁,不要骂老板,这只是说明你没有完全胜任项目经理的职位,还是赶快想想怎样完善自我,提升内功吧。

    二、新任项目经理的误区

    新任项目经理,由于经验和知识储备的不足,往往会出现相同类型的问题。

    1. 农夫的一天

    有一个小故事,讲的是一个农夫的一天:

    有一个农夫一早起来,告诉妻子说要去耕田,当他走到40号田地时,却发现耕耘机没有油了;原本打算立刻要去加油的,突然想到家里的三四只猪还没有喂,于是转回家去;经过仓库时,望见旁边有几只马铃薯,他想起马铃薯可能正在发芽,于是又走到马铃薯田去;路途中经过木材堆,又记起家中需要一些柴火;正当要去取柴的时候,看见了一只生病的鸡躺在地上……这样来来回回跑了几趟,这个农夫从早上到夕阳西下,油也没有加,猪也没有喂,田也没耕,最后什么事也没做好。

    故事看上去很可笑,但笑过之后,回过头思索一下,故事里是不是也有我们项目的影子呢? 我们将《农夫的一天》换成《项目经理的一天》:

    软件项目经理小赵打算今天完成本周五项目阶段汇报的材料,他打开电脑,想起了还有一个重要的技术问题没有确定最终方案;于是他召集项目技术骨干准备继续讨论,一个钟过去了,还没有结论,这时老板来电话,要去老板办公室汇报工作,原来昨天老板跟客户吃饭,客户说到系统有一项功能无法使用,两周了还没解决;从老板房里出来,小赵继续写汇报材料,没多久,项目组的小张找来要反映项目组绩效考核结果以及加班工资的问题;快下班的时候,销售部经理匆匆忙忙地找到小赵:“快帮我估算一下这个项目的实施成本,明天我要给客户报价”……就这样,小赵一天都忙得不可开交,终于下班了,汇报材料没写多少,重要技术问题也没有解决,客户的问题也没安排处理,绩效考核的问题还要跟部门经理以及人力资源部沟通。唯一完成的一件工作,就是帮销售部估算成本,可惜跟自己负责的项目却没什么关系……经过一天的奋战,问题不但没有减少,反倒变多了。

    这样的一天无疑令人沮丧,但却经常出现在我们的现实中。当高级经理询问怎么还没有提交项目计划的时候,项目经理无可奈何又理直气壮的说:“我很忙啊!”

    项目经理确实很忙,但这是没有效率的忙。其实何止是忙,还“茫”,而且“盲”,“忙、茫、盲”是许多新任项目经理的写照。

    ● 忙:一天到晚都在忙过不停,是为忙碌;

    ● 茫:碰到什么做什么,像个无头的苍蝇,没有计划性,或者无法坚持计划,是为茫然;

    ● 盲:项目经理这一天初始目标究竟要做什么,做着做着就丢了,没有目标性,是为盲目;

    2. 思维转换

    有时候我们会说一个项目经理,不像一个项目经理,那像什么呢?当然是像程序员罗。也就是说,他的职位虽然变化了,但并没有完成相应的角色转换,仍然像程序员那样工作。项目经理之所以会出现“忙、茫、盲”状态,归根到底也是因为他没有实现自己的角色转换。

    角色转换本质上是思维转换。思维决定一个人的行为,项目经理不像项目经理,那是因为他的思维仍然是以前的技术思维,而不是管理者应当具备的管理思维。这就好比一个人在陌生的城市,拿着过时的地图,寻找自己的目标,结果只会是四处碰壁,无所适从。

    表1 技术思维 vs 管理思维

    比较方面

    技术思维

    管理思维

    关注中心

    以过程为中心的思维

    关心每项任务本身,而不是整体目标。不重视计划,对任务缺乏控制。

    以目标为中心的思维

    以终为始。关注整体目标、实现的路线、影响目标实现的因素、各种事件对目标的影响,区分重点。

    事物结构

    局部思维

    过于关注细节,对整个项目工作的内容、完成路线没有概念。上来就干,工作缺乏计划性、条理性。

    整体思维

    采用结构化分析方法,自顶向下,先整体后局部。有时亦采用头脑风暴,先将细节展开再归纳。

    逻辑思维

    以机器为中心的思维

    思想单纯,性格直率。在人际问题上过于讲究逻辑。

    以人为中心的思维

    人是执行项目的主体,关注事情本身,更关注人的价值。学会包容,能与各种不同情格的人打交道。

    决策依据

    完美思维

    不关心进度和成本,只关心完美的功能和代码,并视之为艺术。经常对上一任的工作推倒重来。

    平衡思维

    拒绝渡金,项目不需要艺术。在进度和质量之间取得平衡,在员工个性与团队凝聚力之间取得平衡,在员工、项目、公司和客户之间取得平衡。

    人际关系

    个人思维

    以个人为中心,单兵作战,依赖个人能力。个性固执,工作方法简单。

    团队思维

    你不是一个人在战斗,发挥每个成员的作用比个人埋头苦干重要得多。关注团队分工、配合以及士气和凝聚力。

    实现思维转换需要时间,这期间是一个懵懂的、左右为难的、痛苦难熬的阶段。有些人可以在很短的时间内完成蜕变,有些人却可能一辈子都在这个阶段,这跟一个人能不能改变自己有关。这些不能改变自己的人,理论知识往往也很丰富,说什么都头头是道,可惜的是,这是无效的知识,因为不能用在自己的实践中。这样的人,往往有一定的人格分裂倾向,因为他的知识和他的行为不统一,甚至是矛盾的。知行合一才是学习的最高境界。

    新任的项目经理,别忘了时刻提醒自己,像一个项目经理一样去当项目经理!

    3. 项目经理行为分析

    第一次当项目经理,往往会由于经验不足、项目管理知识的不足以及角色转换等原因,表现出种种不胜任的迹象。

    不胜任的项目经理,通常有以下几种类型:

    (1)刺猬型

    刺猬型的人非常敏感,随时都保持警惕,只要一感觉受到威胁,便会用豪猪般的刺扎向对手,让人避之不及。他们通常自我封闭,坚守自己的地盘,处处表现出来自己是对的,虽然其实他自己也并没有底气。他不会让别人看到自己的脆弱。

    刺猬型项目经理不允许别人干涉自己的项目,哪怕是自己的上级。如果领导询问项目中的某个问题时,他会非常明确的告诉你,那不是我的问题,那是客户的问题,或者是公司制度引起的问题,或者是领导你干预项目造成的问题。总之,我一切都做得很好。

    刺猬型项目经理的这种反应通常是不自信的反应。小猫在害怕时,总是拱起背,把全身的毛都竖起来,让自己看起来更强大,但老虎永远不会这样。

    (2)绵羊型

    绵羊型项目经理的性格非常温顺,他们语气平和,慢条斯理,不急不躁。对待下属非常友好,在他们心里,似乎没有好和不好、对和不对,这些对他们都不重要。项目每天都很平静,似乎永远不会有暴风雨的到来。当上级提出要求时,他们永远都是好的,至于做成怎么样,只要尽力了,那有什么关系呢?

    绵羊型项目经理通常工作缺乏计划性,即使有计划,也只是应付上级而已。看到什么事情,就去做什么事情,除此之外,还有什么其它的办法吗?

    (3)猴子型

    想像一下孙悟空的行为就对猴子型项目经理有了大致的认识。他们技术能力强,很有激情,非常聪明,非常自信。但他们往往性格冲动,做起事来横冲直撞,不讲究方法。

    猴子型项目经理悟性很强,进步会很快,他们最终会克服自己的不足,像孙悟空一样,取得正果。这一刻,他已经不是猴子了。

    刺猬型和绵羊型项目经理,他们往往缺乏自信,其管理模式一般是被动式的,做事没有计划性,有什么事就做什么事,就像条件反射一样,只会对外界刺激做出反应。

    猴子型项目经理则是主动式的管理,他们充满自信,但往往由于经验不足,过于盲目,对问题考虑不周。同时由于冲动的性格,在团队中并不受欢迎。

    这三种类型都是不胜任的表现,那怎样才是胜任的类型呢?如果还是用一种动物来比喻,我觉得应该是“头狼”,也就是狼群的首领。

    暂时的不胜任不要紧,关键是要有进步。如果一个项目下来,除了很疲惫,你没有感觉到自己有一些积极的变化,那你的危机也要来了。要知道,项目经理并不是“铁饭碗”,虽然公司倾向于选用有经验的项目经理,但当你明显不胜任时,领导不会再在你身上押上赌注,他们宁可重新冒险一次,因为他们不想“两次踏进同一条河流”。

     

    4. 心态

    新任项目经理没有管理经验,不胜任是可以理解的。也许你认为公司应该给你更多的培训再上岗,但往往形势是箭在弦上,在没有更多资源的情况下,领导把这个成长的机会给了你。

    可怜的是公司老板,他的项目成了你的试验田。实际上,公司提拔你做项目经理,就是花巨资送你去培训学校,不是吗?我一直认为,由一个不合格项目经理负责的项目,相比由优秀的项目经理来带,实施成本可能多出50%,甚至更多。不合格的项目经理就像一个给项目减肥的机器,使得肥肉变瘦肉,瘦肉变骨头,骨头变渣滓。

    项目经理应该学会感恩。要成为优秀的项目经理,应该有好的心态,而感恩是一切好心态的基础。你只知道自己压力大,却不知道你让老板少赚了多少钱!是老板交学费帮你从一个初出茅庐的项目经理,培养成了一个合格乃至优秀的项目经理。

    我见过不少新任项目经理,对公司满肚子怨气,好像是公司一手造成他的项目问题百出,仿佛领导和老板成了他的敌人,刚做完项目甚至还没有做完项目就果断匆匆辞职,带着公司用无形成本换来的宝贵经验,绝决的离去,换取更快的升职加薪。设想一下你是老板,不知会作何感想?

    感恩是一个人最重要、最美好的品质之一。网上有一个经典感恩的段子:“…感谢鞭打你的人,因为他激发了你的斗志,感谢遗弃你的人,因为他教导你该独立,…凡事感激,学会感激,感激一切使你成长的人!” 而你的领导和你的老板,他们既不是鞭打你的人,也不是遗弃你的人,而是培养你成长的恩人,我们有什么理由不感谢他们呢?

    从程序员到项目经理(三):认识项目经理
    (阅读提示:本文比较长,如赶时间,可直接跳到第二节的第四点,即“西西吹雪的六种能力模型”)

    在希腊德尔斐的阿波罗神庙上,刻得着一句神秘的箴言:“认识你自己”。从某种程度上来说,我们都是自己的“最熟悉的陌生人”。认识自己的位置,是每个人获得成长的第一堂课。一个人的位置,对其言行的影响是至关重要的,俗话说:“屁股决定脑袋”,虽然听着粗俗,却饱含人生哲理。既然我们屁股在项目经理的位置上,就应该像项目经理一样去思考问题,做事情。

    一.项目经理的处境

    经过数年的打拼,怀着美好的向往,我们终于成了他——项目经理。然而,梦做到最真的时候,往往也是梦醒的时候。

    项目经理其实也是悲情人物。从“程序猿”到项目经理,可以说是刚出虎穴,又入狼窝。要知道,做一个合格的项目经理,比成为一个优秀的程序员,还要难得多。

    本来以为当上了项目经理,王子和公主从此就可以幸福的生活在一起了,没想到,跋涉的路才刚刚开始。我实在不想打碎这美好的梦想,这有些残忍,但清醒的痛着,总好过麻木的睡着。更何况人生本来就是一个接一个的杯具,每个角色都有他的难处,我们只能接受这个现实。人生就像登山,当你到达一个山头时,发现还有更高峰,一山还比一山高。

    王子和公主,一直在路上。

    1. 高和低

    没有成为项目经理之前,期望着当上了项目经理,可以拿着更高的工资,被别人尊敬的称呼为某某经理,还可以干着更少、更简单的活——指挥别人干活,这谁不会啊?

    然而,人生不如意十之八九。更高的工资,应该是有的,但往往还不会达到让你眼前一亮的数字。被尊称为经理,也是应该的,Project Manager,名正言顺的经理。然而,在大部分公司里,项目经理也就是像弼马温一样的小官,明白真相之后,又难免有一些失落。至于干更少、更简单的活,那就只能说是痴人说梦了。

    事实上,在兴奋过后,等你翻到硬币的另一面,你会看到和你想像不一样的高和低:能力要求高、职位低。

    (1)能力要求高

    能力要求高不高,口说无凭,我在网上随便找了一个软件项目经理的招聘信息,要求如下:

    职责范围:1、负责软件项目管理及计划实施;2、具备较强管理、协调及沟通能力,帮助开发人员解决开发过程中遇到的技术问题,做好日常的开发团队管理工作;3、与各团队协同工作,确保开发工作正常顺利的开展;4、具备较强的分析问题、解决问题的能力,能够解决项目团队在开发过程中遇到的技术难题;任职要求:1、计算机相关专业,4年以上JAVA软件从业经验,2年以上开发经理或团队管理经验;2、精通java、jsp、HTML、JS、xml、AJAX编程语言,精通Struts、Hibernate、Spring、IBatis等常用框架技术;3、精通中间件技术,对Websphere、WebLogic等有很深的了解;4、快速适应工作环境,应变能力强,抗压能力强;5、重视成本和进度控制,合理有效利用资源,有较强的责任心;6、熟悉Android开发、Hadoop技术者优先考虑;

    上面的要求写得比较随意,我帮他整理一下,并点评一番:

    表1 项目经理职责要求

    类别

    职责/要求

    点评

    专业技术

    精通多种编程语言和技术框架;精通中间件技术;熟悉Android及Hadoop。

    项目经理必须是技术专家,也许你自己不用写代码,但你必须能指导下属,解决技术问题。必要时,还得参与做系统架构和系统分析。

    管理技能

    项目整体管理;成本管理;进度管理;资源管理;团队管理;沟通协调能力。

    难道风险管理、质量管理、采购管理就不需要了吗?九大领域一个都不能少。

    个人内在

    适应能力;应变能力;抗压能力;责任心;分析问题解决问题的能力。

    ①     适应能力:像变色龙。能适用不同公司文化和氛围,不同性格的同事,特别是上司。②     应变能力:像变形虫。项目过程中会出现各状况,必须能调整自己、调整计划,以适应变化。③     抗压能力:像驴子。项目管理压力很大哦,天塌下来要也扛着。④     责任心:项目出问题,基本上责任都是你的,决不可推卸责任,勇敢的去解决问题吧,不要辜负领导的重托。⑤     逻辑思维:项目经常会出问题的,所以你必须思维清晰,能够客观的分析问题和解决问题。

    相关经验

    4年开发经验+2年管理经验

    老板可不想冒险,把项目给你去做试验田。

    怎么样,要求很高吧?能完全达到这样的要求,我想去铁道部当个CIO应该是没什么问题了。即便如此,对于项目经而言,这些要求也没有哪一项是多余的,也就是说,项目经理必须成为一个超人,最好是像《蜘蛛侠》里面沙人那样,可以随心所欲的变化自己,穿越一切障碍,拥有无穷的威力。

    (2)职位低

    说职位低,有以偏概全之嫌。在项目型组织结构的公司中,项目经理的职权还是很大的,项目经理一般直接向总经理汇报工作。但在IT行业中,比较少采用项目型组织结构,大部分是矩阵型或职能型的组织结构。在这种架构中,项目经理基本上就是最小的官了。

    2. 大和小

    项目经理之所以需要很强的个人能力,归根到底是由项目经理的责任所决定的。项目是一种个人责任制的管理方式,项目经理是项目组的核心,责任无疑很大;与之相对应,其权力又是比较小的,这让项目经理的处境更加困难。

    (1)责任大

    项目经理作为项目组的最高领导,对项目的成败起着至关重要的作用。对项目的目标和实施过程中的一切问题,负有最终的责任。影响项目成败的因素也许有许多,但不管是什么原因,最终的责任会落实在项目经理身上,领导会说,项目经理不给力。

    (2)权力小

    项目经理的正式权力包括指挥权、人事权、财权、技术决策权以及采购权等,项目经理一般在某一限度内具有完全的权力,无需沟通汇报即可自行做出决定;在超出限度时,则需要与高级经理或职能经理商议决定。在一个矩阵型组织结构的公司中,项目经理的权力大致如下表所示:

    表1 矩阵型组织中项目经理权力情况

    权力类型

    完全的权力

    部分权力

    指挥权

    对项目内的人、财、物的调度安排,可以自主决定。

    对项目结果产生较大影响时,需与高级经理讨论。

    人事权

    可以依据公司制度对员工进行考核、奖惩。

    人员的聘用、辞退等决定一般由职能经理安排,项目经理可以作出建议。

    财权

    小额活动经费一般可以自主决定

    达到一定金额需要申请,由高级经理直至总经理审批

    技术决策权

    一般技术措施可以自主决定

    重大技术措施,必须通过外部评审,并请上级领导拍板

    采购权

    小额采购项目必须品

    达到一定金额需要申请,由高级经理直至总经理审批

    乍看上去项目经理权力并不小。但在实际操作中,项目经理权力范围的这个限度往往比较小,并不足以保证项目经理推动项目顺利开展,项目经理必须花去大量的时间去与上级领导沟通、汇报、提出建议、争取支持。在有些公司,甚至连项目组聚餐也要向上汇报请示。项目经理的这种处境往往会导致其工作畏首畏尾,做事犹豫不决,久而久之,失去了对工作的激情。

    3. 夹心饼

    项目经理的位置是比较尴尬的。下面的兄弟要你多争取一些奖金;领导要你经费更省一些;客户要你更快一些;用户要你的产品更好用一些。在员工面前,你代表老板;在老板面前,你代表项目组员工;在客户面前,你代表公司。你代表了很多人,就是没有代表自己的时候。

    项目经理就是一个不折不扣的夹心饼。做人难,做项目经理更难啊。

    图1 项目经理成了夹心饼

    4. 为什么还要做项目经理

    也许你会问,既然项目经理这么难、这么惨,好像比“程序猿”还要苦逼,那我为什么还要做项目经理呢?这看上去不是个问题,“人往高处走,水往低处流”嘛,高处虽然艰险,向上追求的脚步却不能停止。无限风光在险峰,还是别埋怨攀登的辛苦,好好享受一路的风景吧。

    当然,人的一生有不同过法,有些人喜欢在泳池中游水,有些人在热衷于在大海的激流中冲浪,还有些人,一辈子也不会游泳,他们只是偶尔到河边洗洗手,用冷漠或者好奇的目光看着那些乘风击浪的人们。每种活法的选择权在自己手上,一旦选择,无怨无悔。

    二.项目经理素质模型

    1. 素质模型的作用

    谈素质模型是一件很严肃的事情。因为素质模型就像一面镜子,项目经理拿来一照,可以发现自己的优势和弱点。只有扬长补短,才能在职业发展之路上步步高升。

    管理方面的素质模型很多,但不是每一个都是客观的镜子,如果不能在镜中看到一个真实的自己,那它也就失去了应有的价值:

    如果它是一面哈哈镜,那看到的将是一个变形的自己,无法作为自己的参照;

    如果镜子太小,就只能照到自己的局部,会导致产生盲目的悲观或乐观;

    如果镜子太大,可能会看到太多无关的东西,反倒干扰了自己的视线。

    2. 他山之石

    (1)PMI知识体系模型

    PMI将项目经理应具备的知识和技术分为五类,即:项目管理知识体系,应用领域知识、标准与规章制度,理解项目环境,通用管理知识与技能,人际关系技能,如下图所示:

    图2 PMI的项目经理知识技术体系

    (2)麦克利兰的素质模型

    美国心理学家麦克利兰经过研究提炼并形成了21项通用素质要项,并将21项素质要项划分为6个具体的素质族,同时依据每个素质族中对行为与绩效差异产生影响的显著程度划分为2~5项具体的素质。6个素质族及其包含的具体素质如下:

    ①管理族,包括团队合作、培养人才、监控能力、领导能力等;

    ②认知族,包括演绎思维、归纳思维、专业知识与技能等;

    ③自我概念族,包括自信等;

    ④影响力族,包括影响力、关系建立等;

    ⑤目标与行动族,包括成就导向、主动性、信息收集等;

    ⑥帮助与服务族,包括人际理解力、客户服务等。

    (3)管理者胜任特征模型

    胜任力是指任何直接与工作绩效有关的个体特质、特点或技能等,在本质上也就是应该具备的素质组合。有学者利用物元分析和可拓评价方法建立了基于管理技能、个人特质和人际关系3个维度的胜任特征物元模型。

    ①管理技能的维度,包括团队领导、决策能力、信息寻求和市场意识等;

    ②个人特质的维度,包括影响力、自信、成就欲、主动性、分析思维和概括性思维等;

    ③人际关系的维度,包括人际洞察力、发展他人、关系建立、社会责任感和团队协作等。

    (4) 四种能力论

    Robert hogan和Rodney B.Warrenfeltz研究指出管理人员的素质可以分为4种,分别为:自我管理能力、人际关系能力、领导能力和商业能力。

    ①自我管理能力,包括自我尊重、正确对待权利的态度和自我控制等;

    ②人际关系能力,包括换位思考、正确预计他人的需要、考虑他人的行动等;

    ③领导能力,包括建立团队、维持团队、激励团队、建立共同愿景和巩固团队等;

    ④商业能力,包括制订计划、管理预算、绩效评估、成本管理和战略管理等。

    3. 几种素质模型的分析

    上面这些模型,都是被广泛认可的模型,我本人对四种能力论,更是情况独钟。为了找出一个适合项目经理学习修炼的模型,我们有必要对这几种模型进行评价。

    首先确定评价的指标:

    (1)针对性:是否适合于项目管理领域;

    (2)完整性:是否太过宽泛或狭窄;

    (3)实用性:是否适合于项目经理修炼。

    表2 几种素质模型的评价

    模型

    针对性

    完整性

    实用性

    PMI的项目经理知识技术体系

    太小

    麦克利兰的素质模型

    较差

    太宽

    较差

    管理者胜任特征模型

    太宽

    较差

    四种能力论

    太宽

    那我们能不能找到一种这三个指标都吻合的模型呢?

    4. 西西吹雪的六种能力模型

    “六种能力模型”力图在针对性、完整性和实用性方面达到最佳。六种能力分别是:知识、技能、逻辑思维、执行力、心智成熟和领导力。这六种能力是一个有机的整体,如下图所示:

    图3 项目经理的六种能力模型

    (1)人、事结合

    管理,就是管人理事,这个理念已经深人心。这个模型首先就是一个管人理事的素质模型。

    从“理事”的角度来讲看,项目经理应当具备四大素质:

    ● 知识

    必须具备项目管理的理论知识,所处的行业知识, 以及专业知识;

    ● 技能

    光有知识是不够的,还要能知道怎么做。主要有项目管理技能、沟通表达技能、写作技能、专业技能等。

    ● 逻辑思维

    项目经理必须具有较强的逻辑能力、思维清晰,对项目任务和要做的工作,随时都有清晰的分类和列表。逻辑思维能力有很多种,如果要挑出两种对项目经理最重要,我觉得是归纳能力、判断力。

    ● 执行力

    项目经理本人必须具有很强的执行力。如果项目经理像个蔫老头,整个项目的执行结果可想而知。

    从“管人”的解度来讲,项目经理应当具备两大素质:

    ● 心智成熟

    要管人,首先必须学会与人相处,心智不成熟的人,与人相处往往会无所适从。心智成熟,也就是要管好自己的内心。自己都管不好, 怎么管别人呢?

    ● 领导力

    项目不是一个人的战斗,有些项目经理,只顾自己埋头干活,乐不滋滋,下面的同事却不知道每人要做什么,这是缺乏领导力的表现。余世维说:“管理就是让别人完成事情”,“真正厉害的人不是自己累死,而是要让手下做事情累死,这个才叫本事”,“优秀的管理者不会让员工觉得他在管人”。这三句话,可以说是领导力的三种境界。

    简而言之,项目经理就像一个贤妻良母,要上得厅堂,下得厨房。上得厅堂意味着,项目经理要擅长与人打交道,也就是“管人”的要求。下得厨房则意味着项目经理懂技术、懂业务,能把复杂的事情理清楚,并解决各种问题,这就是“理事”的要求。理事主要靠智商,而管人则主要靠情商。

    (2)内、外兼修

    这个模型还是一个内外兼修的模型。古人云:“胜人者力,自胜者强”,说的其实就是一个人的外在修养与内在修养的关系。

    战胜外在的事物,你需要是“力”,因此模型也有两个力:执行力和领导力。有这两种力,我们可以在管人、理事都做得很好。

    要战胜自己,则非要靠一个人的内在修养不可。因此模型中,有四项个人内在素质的修炼:知识、技能、逻辑思维和心智。

    从表面上看,“自胜”似乎比“胜人”更牛一些。但是从一个人成长的角度来看,我们主张要先“自胜”,再“胜人”。如果以树类比,“自胜”是根,“胜人”则旧枝干,一棵没有发达根系的树,是不可能长成参天大树的。所以不要让自己一开始就显得很牛,而是首先让自己成为一个真正的牛人,否则大树会过早夭折。

    (3)从独立到互赖

    一个人有成长过程可以分为三个阶段:依赖期、独立期和互赖期。每到一个新的阶段,都是一次巨大的飞跃。

    ●依赖期:围绕着“你”这个观念——你照顾我;你为我的成败得失负责;事情若有差错,我便怪罪于你。

    ●独立期:着眼于“我”的观念——我可以自立;我为自己负责;我可以自由选择。

    ●互赖期:从“我们”的观念出发——我们可以自主、合作、统合综效,共创伟大前程。

    也许你已经注意到了,在素质模型里面没有依赖期,这是因为在依赖期的人是无论如何也成不了项目经理的。这个模型,是一个从独立期走向互赖期的素质模型。

    在独立期,我们主要擅长做“理事”的工作。我们是技术英雄,可以把每件事都做得很完美;

    在互赖期,我们的精力转向了“管人”。我们懂得如何与各种不同类型的人相处,如果驱动团队为一个共同的目标而努力。

    (4)层次分明

    这个模型是还是一个层次分明的、渐进的模型。从知识到执行力,实际上是一个从“知道”到“去做”的过程,而从心智成熟到领导力,是发挥团队力量的两个阶段。

    图3六种能力的层次

    从程序员到项目经理(四):外行可以领导内行吗
    一.从几个招聘要求说起

    在上一篇中,我举出了一个招聘需求,引起一些朋友的争论。既然招聘的是项目经理,为什么需要那么多专业技能呢?

    在百度上招聘频道搜索“软件项目经理招聘”,可以查到8500多条类似的招聘信息。我们看看国内软件行业老大东软集团的招聘条件:

    工作职责:带领团队完成需求分析,开发计划制定与跟踪,项目组关键技术问题解决,负责项目QCD。岗位要求:1、3年以上软件开发项目经验,2年以上项目管理经验;2、熟练掌握JAVA、WEB开发,精通基于Oracle/Mysql数据库的MIS系统开发;3、具有较强的沟通、组织能力和较好的文字表达、写作能力;5、有医疗业务开发经验者优先。

     

    显然,东软公司也是要求具有较强的专业技能的。当然,也许东软公司太大了,不具有代表性,那么我们再看一个比较小的公司,你绝对没听过(我也没听过),广东广风隆电子科技有限公司:

    任职要求:1.能很好的把握开发质量和项目进度,规避风险。2.具有较强的语言和文字表达能力、沟通协调能力、良好的团队合作精神。3.具备至少3年项目管理经验或大型系统开发实施经验的优先。4.掌握JAVA技术,能熟练应用J2EE,Spring,Struts,Hibernate等开发和测试。5.熟悉基于java的B/S架构应用技术。6.熟悉基于Tomcat、WebSphere、weblogic等应用服务器的开发;7.熟悉大型数据Orecle/SQL Server等,熟练掌握存储过程编写、数据库表设计。8.熟悉unix/Linux操作系统。9.具备软件团队管理经验,熟悉软件开发流程,能够独立完成项目实施的优先。10.具备一定的系统框架设计、熟悉开发流程,具有的良好的需求分析、项目设计、规划能力。13.有如下经验者优先考虑:a.熟悉BIEE,或有BI项目开发实施经验b.对BI/DW的概念和架构有比较深入的了解,熟悉维度模型架构c.熟悉Oracle数据库开发,或有ETL工具ODI经验,精通SQL

    d.有基于java技术项目管理经验的优先,教育行业背景优先

    哇啦啦,这个更不得了。这究竟是招程序员还是招项目经理,我也快被弄迷糊了。看来中小公司比大公司更看重专业技能。

    当然,我再多举一千条也代表不了所有的企业。但诸位如果有时间一条条看,会发现绝大部分公司对“软件项目经理”这个职位,都对专业技能有较高的要求。那么,传说中的“外行领导内行”究竟是不是真的?外行真的可以领导内行,带领项目走向成功吗?

    二.外行 vs 内行

    1. 优势劣势分析

    外行和内行究竟谁更适合当项目经理?那些招聘要求似乎已经为我们给出了答案,最少在软件行业内行项目经理更占据优势。然而,外行的项目经理往往也有其独特的优势,比如,他们往往更有大局观,能跳出技术本身看待问题,有更强的领导力等等。事实上,外行领导内行的现象,在国家大型建设工程或科研项目中要屡见不鲜。据说,我国的原子弹工程就是聂荣臻元帅领导的,而聂帅是不懂核物理的。

    如果拿外行和内行项目经理来PK,并不是一件容易的事情,因为每一项都不是绝对的,这就如同比较男人和女人谁更适合做厨师一样。当我们拿两者PK的时候,其实包含了一些隐含的信息,就是这个外行的项目经理比内行项目经理,更加懂得管理、情商更高,否则的话,内行项目经理会毫无悬念的胜出,也就没有比较的必要了。

    基于这些隐含的信息,我们试着比较一下两种项目经理的优秀和劣势:

    项目经理类型

    优势

    劣势

    外行

    一般具有更强的领导力,更善于激发员工的士气、战斗力;一般具有更强的谈判能力、资源协调能力,客户和上级领导满意度会更高;一般具有更强的沟通汇报的能力;更容易跳出技术本身,看清问题本质;一般更善于权衡轻重缓急,更善于取舍。

    项目详细计划要依靠技术骨干,对其评估的准确性无法做出自己的判断;无法对技术人员进行辅导;无法对技术问题做出分析判断,帮助解决棘手问题;无法对技术人员进度拖延原因做出准确分析、不能很好控制项目;容易造成瞎指挥;容易和技术人员互相看不起对方。

    内行

    外行的劣势往往是内行的优势

    外行的优势往往是内行的劣势

    2. 技术决定论的误区

    所谓内行与外行是纯粹从技术的角度来看问题,单纯讨论内行好还是外行好,其实也暗含着一个前提,就是技术决定项目的成败。而实际上,一个项目能否成功的影响因素,远不止是技术,对一个项目经理的素质要求也远不止技术。同是外行或内行来带一个项目,会由于个人修养与经验在差异,项目结果可能相差很远。因此单纯说外行好,还是内行好,是没有意义的。

    3. 综合素质决定论

    问题的关键其实不在项目经理是内行还是外行,而在于他的综合素质。无论是外行还是内行,只要谁的综合素质更高,谁就是更优秀的项目经理。

    上一篇我们讲到项目经理的六种能力模型,也就是说,一个优秀的项目经理,应当具备六个方面的素质,即:知识、技能、逻辑思维、执行力、心智成熟和领导力。

    在知识层面,包括专业知识、行业知识和管理知识。外行项目经理在专业知识和行业知识方面已经输了,但在管理知识方面按默认值,外行赢了。

    在技能导面,包括专业技能和管理技能。外行项目经理在专业技能也又输了,同样管理技能方面,又略胜一筹。

    现在打成了平手。剩下的,要拼逻辑思维、拼执行力、拼心智、拼领导力,这就和内行外行无关了,鹿死谁手,要看个人的修养。

    因此,项目经理的比拼,拼的不只是管理知识或专业知识这一个方面,而是综合素质的比拼。

    三.外行,你凭什么

    1. 唐僧的团队

    外行,也就是不懂专业知识技术,显然不但不是什么优点,反而是一个项目经理的极大缺陷。那为什么领导还会置这么大的缺陷于不顾,任命一个外行为项目经理呢?换一个角度,也就是说,一个外行,在什么情况下,可以成功的管理一个软件项目呢?

    一件事情的发生,总有他的内部原因和外部原因。具体到这个问题上,也有它的内因和外因。

    (1)在内部因素上,外行项目经理必须具有更高的综合素质。

    现在流行分析西游记中的取经团队,其实也是一个典型的外行领导内行的团队。到西天取经,靠的是降妖服魔的本领,显然唐僧是个外行。但是,唐僧并不是一无是处,相反,他的综合素质很高。他外柔内刚,意志坚定,目标明确,还精研佛法,具有很强的人格魅力,因此他的那些徒弟才能凝聚在他周围,虽历尽千难万险而无悔。

    (2)在外部因素上,必须有合理的人才结构作为支撑。

    唐僧虽然不会打怪,但是孙悟空可以,补齐了唐僧在这方面的不足。试想,如果他的徒弟都不能降妖,任凭唐僧的领导力再强,也注定最终只会被妖怪吃掉。同样一个外行的项目经理,在他的团队中,必须可以信赖的技术骨干,像孙悟空一样能在关键时候解决问题,这些骨干一般就是项目中的组长、系统架构师或者系统分析师,必要时可能要设置项目副经理之职。如果团队中没有技术骨干,都是一些经验不足还不求进取的程序员,那除非项目超级简单,否则项目经理纵然有诸葛亮的才华,也无济于事。

    2. 规模决定一切

    在上面两项条件都具备的情况下,只能说明外行可以担任项目经理了。站在项目本身的角度,除了这两项因素,往往还跟以下方面有着紧密的关系。

    (1)项目规模:规模越大,采用外行项目经理的机率越高。

    (2)项目所在行业:在建筑、施工、水利等传统行业,采用外行项目经理的机率更高。

    (3)项目的技术难度:在项目规模不大时,如果技术难度越大,采用内行项目经理风险更小。

    (4)项目进度要求:时间要求越紧,更倾向于采用内行项目经理。

    (5)项目管理的层次:有些项目层层分包,对于上面次层的公司,项目不需自己实施,只需对项目进行监管,项目经理自然也不需要很强地专业技术了。但对于底层实施单位而言,项目经理懂技术就很有必要了。同样,有些大型项目分成若干个工程,每个工程又包括若干个子项目,也是类似的情况。

    在这些因素中,项目规模是具有决定性的因素。项目规模足够大的时候,也就有足够的经费来配备充分的人才。至于其实方面,其实只是表现而已。

    三.透过瓶子看软件行业

    为什么软件行业外业项目经理相对较少呢?这与软件项目本身的特殊性有一定关系,但在一定程度上也折射出软件行业的现状:

    (1)软件项目规模不够大

    在软件行业,几十万的项目很常见,几百万上千万就是大项目了,项目的利润率很低,很多中小型企业都生存在赢利的边缘。据工信部统计,2011年上半年我国软件行业利润仅占软件业务收入的1.28%。这么低的利润率,估计比东莞的制鞋厂还不如吧。而几百万上千万的金额,对建设、国防这些行业来说,简直不值一提啊。前几天太极集团1.99亿中标铁道部IT项目,大家都不服气。也是,人人都在喝汤,你凭什么搞特权吃肉?

    (2)成熟的项目经理相对紧缺

    软件行业小项目太多,对项目经理的需求量是非常大的,与此同时,成熟的项目经理相对很少。所谓“千军易得,一将难求”啊。当然,即使牛B的项目经理有了,其收入要求也不会低,这是小型项目难以承受的,只能退而求其次,找一个性价比更高的项目经理,或者干脆拔苗助长,找一个不错的程序员来带吧。

    从程序员到项目经理(五):程序员加油站,不是人人都懂的学习要点
    学习是一种基础性的能力。然而,“吾生也有涯,而知也无涯。”,如果学习不注意方法,则会“以有涯随无涯,殆矣”。

    一.学习也是一种能力

    看到这个标题,有人会说:“学习,谁不会?”的确,学习就像吃饭睡觉一样,是人的一种本能,人人都有学习的能力。我们在刚出生的时候,什么也不知道,是一张真正的白纸,我们靠学习的本能,学会了走路、说话、穿衣服…后来,我们上学了,老师把书本上的知识一点一点灌输到我们的脑子里,我们掌握的知识越来越多,与此同时,我们学习能力却好像越来越差了,习惯了被别人喂饱,似乎忘记了怎么来喂自己了。

    学习本来只是一种本能,算不上什么能力,然而,经过二十多年的不断学习,学习反而成为了一种真正的能力,因为我们慢慢失去了它,它就更显得珍贵。

    在学校里我们基本上被动式学习,然而走出了象牙塔之后,不会再有人对你负责,不会有人主动教你,我们需要主动的学习。所谓的学习能力,其实就是自主学习的能力。

    几年前,曾有一本风靡管理界的书,叫《第五项修炼》,这本书倡导建立学习型组织,因为从长远来看,一个组织唯一可持续的竞争优秀,就是比竞争对手更快更好的学习能力。

    一个公司如此,一个人又何尝不是如此?众所周知现在是一个知识爆炸的时候代,知识更新非常快。据说,一个大学毕业生所学习到的知识,在毕业之后的2年内,有效的不过剩下5%,更何况我们的学校与社会需要严重脱轨。我们赖以立足的,不在于我们现在掌握了多少知识,而是我们有多强的学习能力!

    学习不但是一种能力,而且是一种至关重要的能力,而这种能力的核心,就是学习的方法和心态。

    二.买书是最划算的投资

    古人云:“书中自有黄金屋,书中自的颜如玉。”这说明先贤们早就认识到,买书是最划算的投资了。

    当我刚出道的时候,拿着非常微薄的工资,有一次我向主管抱怨道:“现在的书真贵啊,这点工资连饭都吃不起,更别说买书了!”主管对我说:“不要吝惜买书的钱,宁可忍着不吃饭,也不要忍着不买书,因为买书是回报率的最高的投资了。”

    主管的话让我非常震动。后来,我看到喜欢的书时,再有没有手软过。我不断的学习,开发能力也不断的提高,工资水平也获得了大幅度的提高。一年后,我一个月工资的涨幅,就足够买两年的书了。你说,还有比这更划算的投资吗?

    一本书,哪怕只有一页纸是有用的,它将所产生的潜在价值,也会远远超过书本身的价格。当然,书不在多,能踏踏实实消化掉一本好书,可能比泛泛而读10本普通书,要更有价值得多。

    三.多读经典书

    十年前,我刚进入IT行业的时候,真是求知渴,每星期都要往购书中心跑,可惜的是,那时给程序员看的书不像现在这么多,高质量的书就更少了。当时我印象中比较经典的书籍就是《Windows程序设计》、《COM本质论》、《Java编程思想》,还有就是谭浩强的《C语言程序设计》。其它充斥书架的,就是类似于《21天精通XXX》、《XXX从入门到精通》、《XX宝典》这样的书籍。

    回首往昔,令我比较郁闷的一件事就是在我最有学习动力的时候,看的高质量的书籍太少,就好像是在长身体的时候,天天吃的是没营养的泡面。当然,这跟没有人指导也有很大的关系,独自一个人学习,让我走了很多的弯路。

    软件开发方面的书籍,我大致将其分为三类:

    (1)浅显的入门类书籍。

    这类书的标题往往是《XX天精通XXX》、《XXX从入门到精通》、《XX开发实战》等,这类书往往从软件的安装讲起,喜欢翻译帮助文件。有人批评这类书为烂书、毫无价值,这并不公平。至少我本人,也曾从这些书中学到一些东西。即使是21天系列书,也有适合看的人群,只不过,它一般也就只能看21天而已,过后就可以扔到垃圾堆。这类书只适于还没有入门的初学者,从中学到一些入门的招式。这种书在刚起步的时候一般买上一本就可以了。如果你善于使用搜索引擎,这一本书也可以省了。

    (2)国内外高手写的实战类书籍。

    这类书实战性很强,把技术及原理讲得很透彻。比如《windows环境下32位汇编语言程序设计》、《深入解析MFC》、《Delphi深度探索》、《深入浅出WPF》、《深入剖析Asp.net组件设计》等。以前这类书都是从国外翻译或从台湾引进,现在国内高手越来越多,出自国内作者的也越来越多。这类书如果在你学习的每个方向看个两三本,并且通过实践消化掉,那么毫无疑问,你会成为一个优秀的程序员。

    (3)国外大牛写的、揭露本质、有丰富思想的书。

    这类书就是所谓的经典书了,例如《代码大全》、《编程珠玑》、《设计模式》、《重构》、《代码整洁之道》等。经典书就像一个有深度、有思想的朋友,他会给你启发、每次阅读都会有新的收获,这类书具有真正的收藏价值。看经典书永远是正确的选择,它绝不会浪费你的时间,因为经典书是无数人沙里淘金、帮你挑选过的结果。

    然而,阅读这类书并不是一件容易的事情,读者需要有丰富的开发经验,才能与作者产生共鸣。真正能消化经典书的人其实不多,这就好像饮酒,一个新手无论如何也品不出葡萄美酒的醇香。在酒桌上,人人都把杯中酒一饮而尽,当有人点评“这个酒不错”的时候,我只能无奈的苦笑一番,真的是甘苦自知。

    如果一本经典书你看得很辛苦,很有可能就是因为你功力未够,这种情况下不要着急,慢点来,不妨先将其先束之高阁,多看看第二类实战型书籍,过一段时间再回头来看,也许你会有新的惊喜。

    四.不要在上班时间看书

    一个善于学习的人,首先要善于利用一切时间来学习。不知是伟大的雷锋叔叔还是鲁迅爷爷曾经说过:“时间就像海绵里的水,只要愿挤,总还是有的。”然而,当我们从上班时间中挤时间学习时,就千万要注意了,不要在上班时间看书!

    上班时间看书不但是一件很敏感的事情,而且非常吸引眼球,很快就会引起周遭的不爽。首先老板心里不爽,他想:“我给你钱是让你来工作的,不是来学习的!”;其次同事们也不爽:“我们工作都做不完,瞧,这小子真闲哪!”用不了多久,你就会成为被众人排斥的异类。

    当然,你可能会说,“我工作已经做完了,经理没有安排,当然可以学习了”,其实不然。你完成了一件事情,不等于所有的事情都完成了。一个优秀的员工,应该是主动要工作,而不是被动的等工作。工作完成以后,你至少还可以:

    (1)主动汇报给你的经理,请他来检查你的成果,并安排新的任务;

    (2)如果公司这一段时间确实比较闲,没有什么具体的任务,可以进行代码重构、优化;

    (3)你还可以主动请缨,承担额外的工作或更艰巨的任务。

    (4)如果一定要学习,也只能对着电脑屏幕来学习,纸质书最多只能拿来翻阅一下,而不能一直捧着,以免影响到其他人的情绪。

    五、只学习与工作相关的东西

    我曾发现不少程序员在学习方面找不到方向,一会学学C#,一会学学Java,看了最新的编程语言排行榜,又觉得该学C++。这样左抓抓,右挠挠,只会让你觉得更痒。

    学习最忌三心二意。俗话说:“伤其十指不如断其一指”,每门都学一点,还不如专心学好一个方向。这个道理谁都懂,可是又该学哪个方向呢?难道只能跟着感觉走吗?

    不!最实际的方向,应该跟着工作走,工作需要什么,我们就学什么,把工作需要的技能熟练掌握。我们为什么要学习和工作弱相关的东西呢?是为了转行或跳槽吗?可是,如果我们连现在本职工作都不能做好,又怎么能保证到新的岗位、用新学的技能就可以做得更好呢?

    学习与工作需要的的东西,有很多好处:

    首先,可以集中精力,在某一方面钻研得更加深入。所谓“百招会不如一招绝”,有了绝招,你还怕不能在“武林”立足吗?《天龙八部》中的慕容复武功博学无比,最后还不是被只会一招六脉神剑的段誉打得落花流水?

    其次,可以学得更快、更深入,因为学习更具有针对性,而且可以立即在工作中运用,可以马上检验出学习的效果,对存在的问题可以进行深入的研究,因此掌握的知识也会更加的牢固。

    第三,学习与工作结合在一起,工作时间也就成了学习时间,这样突破了三个8小的限制。有人说,我们每天所有拥有的时间可以分为三个8小时,工作8小时,睡觉8小时,另外还有8小时自己可以自由支配的时间。工作和睡觉的两个8小时大家都一样,决定人生高度的是另外这个8小时。当我们把学习的焦点放到与工作相关的知识上时,工作时间中的很大一部分,同时也就成了宝贵的学习时间,这真是一举两得的美事啊。

    六.织网式的学习

    知识的广度和深度都很重要。作为一个程序员,深入把握技术细节,是写出优质代码的保证。但对于一个项目经理而言,知识的广度更显重要。项目中碰到的问题往往是综合性的,只有具有广博的知识,才能快速的对问题进行分析和定位。在程序员通往项目经理的道路上,我们必须有意识的扩大自己的知识面,形成更完善的知识体系。

    每个人的知识体系就好比是一张网,我们学习其实就是要织这样一张网。 我曾看过渔网的编织过程,渔网虽大,也是一个结点起步,一个点一个点的编出来的,编织的过程中,始终只有一根主线。

    学习又何尝不是这样,知识体系的大网也是由许多小的结点组成,要结这样一张网,只能由一个点起步。牵住一条主线,织出一个个的点,由点带出面,最后才能形成这张大网。

    我曾经编写过一个网络信息采集软件,这个软件可以从具有列表页网站中按字段设置采集信息,支持自定义字段、页面多级关联、下载附件、支持多种数据库、可视化定义等特性。刚开始时,觉得这个软件也是一个比较大的功能点而已,后来发现这个不起眼的功能关联着大量的知识点,在开发过程中, 我顺藤摸瓜,各个击破,对很多知识点进行了细致的学习研究,软件开发完成后,个人的知识体系网也进一步得到了补充和完善。

    图1 由知识点形成知识网

    七.问题是最好的学习机会

    日本经营之神松下幸之助曾经说过:“工作就是不断发现问题、分析问题、最终解决问题的一个过程,晋升之门将永远为那些随时解决问题的人敞开着。”可见,工作过程中有问题是正常,没有问题那才是真正的问题。在发生问题能时,能勇于面对问题、解决问题的人,才是公司真正的核心骨干。

    现实中,很多人总是千方百计回避问题,当上司安排一项艰巨的任务时,也是想尽办法推托。殊不知,对于个人而言,其实问题是最好的学习机会。往往那些愿意接受困难工作的人,能力会变得越来越强,那就是因为他们在克服困难的过程中取得了巨大的进步。

    有一次,一位项目经理对我说:“有一个问题,客户有一台HP服务器要装磁盘阵列,没人会做,怎么办啊?”“可以学啊,没有人愿意去吗?”“我都问了,没人想去。”“哦,正好明天我有时间,我也没装过磁盘阵列,那我明天去学着弄一下。”我说的是真心话。第二天早上,当我准备出发时,项目经理告诉我不用我去了,因为项目组好几个同事都想去“学着弄一下”。结果服务器很快就装好了,远远没有之前大家想像的那么困难嘛。更重要的是,在解决这个问题的过程中,大家都学会了怎么装磁盘阵列。

    碰到困难时,迎难而上吧,千万不要拒绝这个最好的学习机会!

    八.经常思考总结

    子曰:“学而不思则罔”。只学习不思考,就会迷惑,难以把握事情的本质。这就好比一个学武之人,只习得其形,而未得其神,难以成为真正的高手。

    一个程序员从入门,到成为高手的过程中,往往要经过几次顿悟。顿悟会让你跳出知识的丛林,一切豁然开朗,仿佛打通了全身的奇经八脉一般奇妙。记得我有一次,顿悟到了一个很简单的结论:“原来高级编程语言中的类库是封装了Windows API来实现的。”后来碰到一些自带类库无法实现的功能时,我就会想到,其实可以通过调用Windows API来实现。利用这个思路,我解决了一些看起来很难的问题,得到老板的赏识,从而很快获得提升。

    顿悟非常可贵,然而它不是随便发生的,而是经过一次次苦苦思索之后、灵光闪现的结果。思考的过程,其实就是将外在的知识内化为自己的知识的过程,而顿悟,则是批量的实现这种内化,将无数个知识点连接在一起,达到融会贯通的境界。

    九、克服“高原现象”

    爱学习的人都会有这样的经历,学习持续了一段时间之后,往往会有一个瓶颈期,长时间似乎很久没有什么进步,于是内心非常着急。

    这种情况实际上这是由人的学习规律决定的一种“高原现象”。据研究,学习者在刚开始进步快,随后有一个明显的或长或短的进步停顿期,后期进步慢,中间的停顿期叫高原期。

    图2 技能学习练习曲线

    在我看来,高原期实质是一个消化期,由于前期的学习积累了太多的知识点,这些知识点在大脑中乱作一团,还没有形成一个知识体系。这时需要一定的时间来消化它,将它融会贯通,经常思考总结可以快速帮你跨过高原期。

    在处于高原期的时候,还可以换一个相关的方向来学习,例如编程语言学不下去了,你可以学习一下设计模式,设计模式也学不下去了,再换成数据库。通过学习这些相关的知识,不但补齐了知识体系中的短板,而且各个知识点之间可以互相启发,帮助你实现顿悟,跨过高原期。

    十、学习要有好心态

    (1)学习要静心

    急于求成是学习过程中普遍存在的一种心态。这可以理解,毕竟作为一个程序员,要学的东西实在太多了,而社会又是那样的浮躁,让人觉得一切都是那样的不安全、不确定,似乎只有学得快一点,才能跟上社会的脚步。

    可是“欲速则不达”,想快快的学,往往会形成东一榔头、西一棒槌的学习方式,每一个点都没有吃透。心沉不下去,知识也会沉不下去。要想成为真正的高手,只能静下心来,一步一个脚印的攀登。

    (2)学习是一个持续一生的过程

    人生的过程,就是一个自我完善过程。

    孔子曾经说:“吾十有五而志于学,三十而立,四十而不惑,五十而知天命,六十而耳顺,七十而从心所欲,不逾矩。”可见孔子也不是天生的圣人,也在不停的学习、进步,从“志于学”到最后“从心所欲,不逾矩”,孔子一共花了55年的时间。

    作为一个程序员,更是需要不断更新自己的知识。我们所知道的东西,就像一个白色的圆圈,圈外则是黑暗的未知的世界。当圆圈越大,所接触到的黑暗部分就越多。我们只有不停的学习,打破更多的黑暗,找到更多光明。

    (3)保持饥饿,保持愚蠢

    看了《乔布斯传》之后,我最喜欢的一句话是“求知若饥,虚心若愚”(Stay Hungry,Stay Foolish),其实我更喜欢它更原生态的翻译“保持饥饿,保持愚蠢”。我们只有认识到自己还很饥饿和愚蠢,才会像没吃饱一样,由衷的需要学习、爱上学习。

    当然,知易行难,知行合一才是学习的最高境界。我也始终是一个学习者,一直在路上。

    从程序员到项目经理(六):程序员加油站 — 懂电脑更要懂人脑
    说起程序员三个字,我觉得既骄傲又可悲。骄傲的是,我们曾经是时代骄子,是一群真正改变世界的人;可悲的是,我们很多致力于改变世界的程序员,却生活在自己的世界里,无法自拔,成为了继“书呆子”之后的“电脑呆子”。电脑本来只是一个工具,我们竟然被其所限制、甚至同化,悲夫!

    一、警惕成为“电脑呆子”

    (1)程序员眼中的自己

    程序员是怎样看待自己的呢?看看园子里的发言,码农、码畜、IT民工、苦逼、程序猿…这样的字眼俯拾皆是。

    在网上曾经广泛流传一首关于程序员的诗,模仿的是唐伯虎的《桃花庵歌》,我们暂且称之为《程序员之歌》吧:

    写字楼里写字间,写字间里程序员;程序人员写程序,又拿程序换酒钱。

    酒醒只在网上坐,酒醉还来网下眠;酒醉酒醒日复日,网上网下年复年。

    但愿老死电脑间,不愿鞠躬老板前;奔驰宝马贵者趣,公交自行程序员。

    别人笑我忒疯癫,我笑自己命太贱;不见满街漂亮妹,哪个归得程序员。

    这首诗的作者不知姓甚名谁,但可以肯定的是,他是一名程序员,因为只有程序员才能这样生动的写出程序员的酸甜苦辣。从诗中看出程序员眼中的自己的形象:敬业、辛苦,每天的时间全部花在写程序和上网;思想单纯;清高不合群,自傲自恋;清贫不得志,自悲自叹。

    (2)别人眼中的程序员

    在别人眼中程序员又是怎样的一个群体呢?在360网站有一个关于程序员形象的热帖,其中回帖的大部分都不是程序员,很多回复都非常生动,没有骂街,可以说比较客观。

    总结一下,大家回复的情况大致如下:

    工作方面

    富有钻研精神,是技术方面的高手,没有时间概念,加班多,辛苦劳累,工作认真严谨,懂制作软件却不懂这软件如何运行更好。

    外在形象

    黑眼袋,红眼圈,睡眠不足,瘦小,邋遢,带眼镜。

    生活方面

    电脑前潇洒自如,世人前胆小腼腆。聪明,思维敏捷,生活刻板。

    性格方面

    “闷骚”这个词不好听,但还是蛮准确的:程序员大多沉默寡言,不善与人交往,但内心却很丰富。性格腼腆甚至孤僻,圈子小,爱憎分明,有点不食人间烟火的样子。

    思维方式

    是一种面向问题的思维方式,逻辑灵敏而严谨,无时无刻不在思考攻克解决问题,善于找别人的问题,却对自己的问题视而不见,不善于解决生活中的问题。

    综合起来,程序员在世人眼中大抵是一个聪明而又迂腐、善良而又刻板的形象,是不是有点像鲁迅笔下的“孔乙己”先生呢?

    (3) “电脑呆子”是怎样炼成的

    上面描述让我想起了一个词:“书呆子”。书呆子是指那些死读书、读死书、不通人情世故、不会用书上的知识变通的人。书呆子是与书待一起的时间太久了,以至于生活在书的世界里,用书里的道理来评价和要求真实的世界。而我们程序员呢,日复一日,年复一年在代码间摸爬滚打。每天用在与电脑交流的时间,比大部分书呆子看书的时间有过之无不及。每天基本上就是“电脑一开,一关就过去了,嚎”。

    俗话说:“带着锤子三年,看什么都是钉子”。当程序员三年,看到谁都当作是电脑。于是产生了计算机时代的“书呆子”,不妨称之为“电脑呆子”。电脑呆子用电脑的时间太久了,生活在电脑的世界里,用电脑的逻辑来要求别人,不懂生活,不懂人情世故。可能你对这样的措辞感到不满,但对多我们身边有些程序员,是不是有几分神似呢?

    悲夫!程序员曾是时代骄子,有非常细腻内心、非常丰富的感情世界、非常聪明的大脑,在世人眼里的形象却是如此不堪!

    孔子说:“君子御物而不御于物”。电脑只是被我们利用工具而已,而我们的思维却被电脑所限制,甚至变得和电脑一样。

    程序员,是该求变的时候了!

    我们再也不要闷骚,将我们的内心美好善良的一面勇敢的表达出来吧!

    我们再也不要苦逼,我们要金钱,更要快乐,我们要工作,更要生活!

    我们再也不要死板,我们可以做出漂亮的程序,同样也可以漂漂亮亮的做人!

    (4) 一个老程序员的肺腑之言

    也有大家会觉得“电脑呆子”这样的词是在骂程序员,是对程序员的不敬,但也许激烈的言辞更能令人警醒。有一个成语叫当头棒喝,据说佛教禅宗和尚接待初学的人常常用棒一击或大喝一声,促他醒悟。

    我曾经是一个程序员,现在仍是。我也曾经是一个真正的“电脑呆子”,我曾独自在黑暗中摸索,花了多年的时间才摸着石头过河—也许我还远未过河。那些曾狠狠骂我的人,我把他们当作我的恩人,因为他们激励和启发了我的成长。当我逐渐走向成熟时,已经错过了无数的机会。

    二、懂电脑更要成为人脑

    (1)电脑逻辑 vs 人脑逻辑

    程序员写代码离不开电脑,沟通、交际又要与人脑打交道,然而电脑与人脑的逻辑在很多方面却是大相径庭。

    比较方面

    电脑的逻辑

    人脑的逻辑

    差异性

    同一个程序在每台电脑上的运行结果都一样

    任务交给不同的人,结果可能大相径庭

    多样性

    每台电脑都一样(换一台电脑编程完全没问题)

    每个人都不一样,人千差万别,因此要适应不同性格的人

    确定性

    程序正确,电脑一定能得到正确结果

    任务明确,做出来的结果与预期可能相去甚远

    思维

    电脑无自主思维。

    个人存在理解力,执行力,判断力等方面的问题

    情感

    电脑没有感情、情绪等因素的影响

    人受感情、情绪的影响

    自主性

    电脑无自主性,完全受程序的控制

    人具有自主性,但行为由很多因素决定

    社会性

    电脑与电脑之间只在严格的逻辑交互,无社会性

    人与人之间的关系微妙

    合作

    1台电脑+1台电脑,运算能力更强

    1人+1人,结果无法预知,团队合作至关重要

    电脑的逻辑简单,所以我们愿意与电脑打交道。如果我们把电脑的逻辑带到与人交往的过程中,那就太“简单化”了,当然也就给人以迂腐、刻板、不懂变通的印象。我们毕竟是生活在人的世界中,我们要懂电脑,更要懂人脑。我们不是只懂电脑异类,而只是更懂电脑的正常人。

    (2)做回正常人

    我曾经很看不起那些不懂技术却八面玲珑的人,看到他们身居高位更是感到愤愤不平,甚至感叹要是生活在西方国家就好了,什么事情都直截了当,不用拐弯抹角。

    然而,经历了无数的挫折之后,我明白了一个道理:“世事洞明皆学问,人情练达即文章”。人家能说会道、八面玲珑也是一种本事啊。不然,我们怎么做不到啊?

    其实并不是这样做很难,而是我们不愿意这样做而已,不愿意为世俗的观念改变自己。没错,现实是世俗的,但现实也是无法改变的,我们只能承认现实,臣服于现实。我在360的那个帖子中看到有一个对程序员的绝妙评价,“程序员是七仙女中的织女”,难道我们真正的要像仙女一样不食人间烟火吗?

    我们不用做仙女,只需要做一个普通的正常人。要顺应人的逻辑,懂人情,明事理,做一个正常人该做的事情,这样并不难。

    莫言在领诺贝尔奖时有一段精彩的发言:

    最后,我讲一个小故事。听说法兰克福是歌德的出生地。在中国,流传着一个非常有名的关于歌德的故事。有一次,歌德和贝多芬在路上并肩行走。突然,对面来了国王的仪仗。贝多芬昂首挺胸,从国王的仪仗队面前挺身而过。歌德退到路边,摘下帽子,在仪仗队面前恭敬肃立。我想,这个故事向我们传达的就是对贝多芬的尊敬和对歌德的蔑视。在年轻的时候,我也认为贝多芬了不起,歌德太不象话了。但随着年龄的增长,我慢慢意识到,在某种意义上,像贝多芬那样做也许并不困难。但像歌德那样,退到路边,摘下帽子,尊重世俗,对着国王的仪仗恭恭敬敬地行礼反而需要巨大的勇气。

    处处与世俗为敌,并不会让世俗变得清高。尊重世俗,也并不意味着失去清高,失去自我。

    不要比拼清高,而要自己生活得幸福。当你能自由的游走于世俗的现实与内心卓尔不群的原则之间时,你也就实现在个人修炼的圆满,成为了一个从内心里幸福的人。

    我们不需要成为清高之人,也不需要成为世俗之人,我们只要成为普通的正常人,一个外圆内方的人。

    从程序员到项目经理(七):程序员加油站 — 完美主义也是一种错
    追求完美是一种可贵的精神,完美主义也历来被认为是一种优秀的品格。可是在项目中,完美主义也是一种错,虽然是一种“美丽的错误”。项目讲求平衡,要的是合格,而不是优秀;要的是70分,而不是100分!

    1.两极分化的程序员

    相信在很多人眼里,程序员都是工作一丝不苟、对代码精雕细琢、精益求精的人。瞧,他们在电脑前面一坐就是大半天,如果不是追求完美之人,谁能这样坐得住板凳?

    可是依我所见,在“追求完美”这个问题上,程序员其实是严重的两极分化。有一部分程序员确实对自己的代码要求很高,他们在编程时,非常注意逻辑是否严谨、运行效率高不高、代码是不是优雅,经常进行代码重构与优化。他们就像有洁癖农村老太,整天扫把不离手,在哪里看到不顺眼的代码,就要改到哪里,如果让他来维护一个系统,多半最后会让他把整个系统的代码全部重构或者重写了一遍。他们是真正的完美主义者。

    还有一部分人,他们写代码似乎只是为了完成任务。他们对自己负责的功能,缺乏计划和设计的过程,想到哪里就写到哪里,最后按下F5,编译通过,欧耶!他们甚至不愿意多点一下自己创建的按钮,更加不会在一个身份证号码文本框中输入一个电话号码来测试一下,最关键的是终于可以向经理交差了,至于代码中多少隐藏的问题,以后再说吧。这让我想起了程序员部落酋长Joel所说的,他们编写的程序“看上去像是给狗吃的早餐,只经狗能吃饱就行了,何必再花钱让食物变得色得味俱全呢?”我们甚至可以想象一下,他们的电脑屏幕上是不是铺满了灰尘,键盘缝里是不是塞满了头发和食物碎屑,电脑桌面的图标是不是如七彩拼图一般,让人眼花缭乱。

    后一类程序员,在数量上似乎占据绝对的优势,对于一个不擅于控制项目质量的项目经理来说,他们的代码最终会成为项目的噩梦。系统一旦投入运行,虫子就会像美国恐怖片中的外星生物一样,源源不断的从鼻孔、嘴巴和耳朵缝里冒出来。

    第二种程序员这种低标准低要求、随随便便的做法,很容易被识别出来是一种不好的倾向,而完美主义不是这样,因为我们从小就被教导要追求完美,完美主义一般被认为是一种优秀的品格,是每个人应追求的目标。

    然而完美主义虽然听上去不错,却并不适合于项目,因为项目的目标是用最少的成本来完成项目,让各方满意,而不是制造一个完美无瑕的产品,以证明个人或公司的能力。显然,完美主义更适合于个人能力的修炼,或者一项没有限期出成果的科学研究,在项目中,完美主义也是一种错,虽然是一种“美丽的错误”。

    完美主义者和随随便便的人,这两种程序员都不是项目的最佳人选,他们是恰好是两个相反的极端,如果让他们负责项目,估计就像玩跷跷板一样,要么压到地底下,要么翘到天空上。但是项目经不起这样的折腾,项目中需要有平衡能力的人,他们很好的把握追求完美的“度”,使得软件功能既能满足客户的应用需求,又不至于要花费过多的精力。可惜的是,这种程序员实在是不多,因为度的把握对程序员而言,确实不是一件容易的事情。

    2.完美不等于质量100分

    程序员心中的完美和项目经理心中的完美并不是一回事,因为两者关注中心不一样。程序员关注的是自己的软件功能本身,力争将软件产品质量做到最好,因此程序员的完美实际上是质量的完美。

    而项目经理眼中,看到的是整个项目,包括质量、进度、成本、范围、风险等方方面面,需要进行平衡,花最少的成本、用最少的时间、达到各方满意、实现项目验收,这就是完美。单纯产品质量一流,而进度拖延、成本超支,这显然不是什么完美的项目。

    其实现代质量管理理论普遍认为,质量并不是越高越好。事实上,市场已经对此无数次给出了证明。很多人骂过微软公司的产品烂,据说乔布斯也曾经大骂windows是坨屎,但微软公司后来却成了软件行业的霸主。

    ISO9000对质量的权威定义是:一组固有特性满足要求的程度。看到了吧,是满足,而不是超出,这非常重要。不要少,少了通不过;但也不用多,多了便是浪费。我们需要的不是100分的质量,甚至也不是一流的质量,而只是满足要求的质量。

    在项目管理中有一个名词叫“镀金”,也就是在产品达到客户要求后,再多做一些额外的工作,让产品更加完美,以进一步提升客户满意度,这在PMBok中是一种被明确禁止的行为。软件质量100分,在项目中不但是一种巨大的浪费,而且几乎是一个不可达到的目标, 只会让项目不堪重负,最后陷入灾难的境地。

    3.合格就是完美

    追求完美本身并没有错,但如果上升到完美主义,时时处处要做到最好,却不一定符合当时当地的条件限制。一个“最”字会害死人,因为“没有最好,只有更好”,如果一味追求更好,其结果可能就如陷入焦油坑的怪兽一般,无法自拔。在这样一个讲求效率的时代,完美主义更是可能会造成机会的丧失。因此,要保持追求完美的心,但又要懂得权衡,不要陷入极端的完美主义的陷阱。

    要完美不要完美主义,本质上是一个度的问题,项目应讲求平衡,避免极端。学过项目管理理论的人都知道,项目管理中有一个“铁三角”,也就是在一定的项目范围的约束下,成本、进度和质量构成三角形的三个端点,为了让三角形面积保持不变,任何一个端点的变动,都会引起其他一个或两个端点的同步变动。这个铁三角本质上就是一种平衡和制约的关系,而完美主义,则只单纯的强调质量,而忽略了其它方面的因素,这显然是一种极端的行为。

    那项目中质量的“度”倒底是什么呢?其实就“合格”二字。合格意味着被认可,却不需要达到优秀的代价。客户认可、领导高兴、员工轻松,这不就是完美吗?可以说项目中没有最好,只有合格,合格就是完美。

    4.“70分主义”

    从小老师和书本就教育我们要追求完美,考试要考100分,90分都嫌太低,那70分还拿得出手吗?

    其实70分不低了,要知道现在大学生的口号是“60分万岁,多一分浪费,少一分作废”。当然这种口号容易被批评为不思进取,但万物存在就有其合理的一面,“60分万岁”也是事出有因。

    在学习方面,我是主张完美主义的,前提是学的是个人感兴趣、有用的、切合实际的东西,可以我们大学的大部分课程,基本上是背道而驰。上课、考试,无非是为了不挂科,顺利拿到毕业证和学位证。这种情况下,60分万岁也就容易理解了。何必要考100分,节省下来的时间,完全可以用来学习自己更感兴趣东西。

    从某种程度来说,做项目也是一种考试:有考试内容(项目范围)、考试时间(进度要求),还有及格线(质量要求和验收标准)。项目的及格线如果用分数来表示,也是60分,既然60分就够了,为什么还果提出“70分主义”呢?其实很简单,因为要想刚好考60分,实在太不容易了,搞不好就会弄个不及格。所以我提出“70分主义”,一种超越完美主义的新主义,力求在及格和完美之间达到平衡。

    项目如果以70分为目标,适当留出缓冲,就可以做到游刃有余,更容易把控。70分意味着已经达到客户的验收要求,已经能投入正常使用,但可能存在一些影响较小的Bug,个别页面效率有待提升,个别操作不是很顺手,系统扩展性一般,代码组织有等进一步优化……这些不完美的地方,就让他们在那里待着吧,毕竟客户已经觉得已经达到目标,何苦自己跟自己较劲,非要达到100分呢?早验收、早收钱,这才是王道!吃饭只用7分饱,项目也是只要70分,“70分万岁”!

    从程序员到项目经理(八):程序员加油站 — 不要死于直率
    直率听上去是一种美好的品德,然而如果不注意区分实际情况,直率可能会成为一把伤人害己的“双刃剑”!

    1.直率是关于说话的问题

    公司曾有一位人力资源经理是从传统行业转过来的,有一次她跟我说:“程序员真有意思,他们全都是一根肠子通到底,大脑不会转弯!”

    还真是这样的,估计没有哪个行业的人员像程序员这样,具有同样的鲜明的性格特征:直率。

    直率很容易理解,其实就是一个关于说话的问题,准确的说这是一个关于说还是不说、以及说多少的问题。 过于直率的人,在说话方面往往有两个特征:

    (1)想到什么说什么

    这是一个说还是不说的问题。显然,不是什么东西都可以随时随地的说,或者对任何人说。可是对于过于直率的人而言,想到了就要说出来,就像俗话所说的:“嘴上没有把门的”,不管好话坏话,不区分场合,不论说话对象是谁,心里想着什么,嘴里马上就出来了。如果是好听话还好,皆大欢喜;如果是让人难堪的话,那就会伤害了别人了。如果是在公众场合让人难堪,人家可能会记住你一辈子。

    (2) 知无不言、言无不尽

    这是一个说多少的问题。“知无不言、言无不尽”毛主席老人家曾大力倡导的,这在讨论具体事情时无疑大有帮助,可是如果作为一种品格,在中国国情(文化氛围)下,还是不宜提倡。话并不是说得越多越好,说得越多,错的机会就会越多,反倒容易被人家抓住弱点或把柄。俗话说:“逢人只说三分话”,对一个项目经理而言,更应是如此。

    在情绪方面,过于直率的人,往往不能很好的掌控自己,在说话时容易显得消极或冲动。

    (1) 消极型

    言语显示出消极的态度,例如给人泼冷水。给项目组成员安排工作时,我最怕听到两种话,一种是说“这个我不干了”,还有就是说“ 我不想干这个”,每次听到这两句话,心里就觉得凉飕飕的,虽然不爽,但作为团队的核心,我只能选择耐心的分析和开导。如果项目经理也以消极的方式来应对,整个团队的士气将会爱到打击。

    (2)冲动型

    虽然人人知道“冲动是魔鬼”,但魔鬼不是那么容易掌控的,人难免有时会说出冲动的话来,以直率著称的程序员更是如此。冲动的主要表现是言辞激烈固执、情绪激动,一旦过头就会变成人身攻击。我经常看到程序员与项目经理讨论问题,后来争论不休,项目经理被迫使用自己的职位权力,强制执行,但这样可能会导致矛盾激化,搞不好程序员就会拂袖而去:“我不干了!”

    无论是消极还是冲动,这都是职场的大忌。这些外在的情绪,在领导的眼中,就是体现出了一个人的工作态度,而在职场上,态度决定一切。对于领导而言,没有积极、合作的态度,就意味着你不能为我所用,那留你何用?!

    2.直率的悖论

    对于直率的人有很多有意思的词语,好听一点的比如“直性子”、“直来直去”、“心直口快”、“快人快语”、“率性而为”,不好听的如“口无遮拦”、“大嘴巴”、“一根筋”、甚至“缺心眼”。可见在要不要直率这个问题上,中国人真的过得很矛盾、很痛苦,有时甚至无所适从。“说,还是不说,这是个问题”。长此以往,对于修养不够的人,造成人格分裂的倾向也不足为奇。

    (1)书上提倡直率,现实鼓励含蓄

    其实好像没有哪本书正儿八经的告诉我们做人要直率,那为什么很多人眼里直率是一种美德呢?我想这是从小到大的教科书对我们潜移默化的结果。书上教育我们世界上主要有两种人:好人和坏人。好人多是直率的人,他们流芳青史,而坏人多是阴险狡诈之徒,他们遗臭万年。对比之下,我们当然想做直率的人了,光明正大,而且要直率太简单了,人人都做到,做的过程中还很爽。

    另一方面,中国人的含蓄又是出了名的。中国是一个讲人情的社会,什么东西最重要?人情和面子。一个成熟的人不会当面伤害别人的感情,不太好的事情喜欢用暗示,以免伤了面子。 不但说话,连中国的诗歌、中国的医学、中国人谈恋爱,也是含蓄的、有点模糊的不清的。

    (2)觉得直率好,却很少有人喜欢别人对自己直率

    假如做一个调查,你是喜欢别人直率,还是含蓄,我相信大部分会选择直率。当别人含蓄的时候,我们会催着让他“有什么话直接说”,等别人说了,如果事情难办,心里又可能犯嘀咕,“这人说话也太直接了,连退一步的余地也没有了”。

    其实我们很少有人希望别人每件事情都对自己直来直去,试想一下,当别人当面揭露我们的缺点时,我们会是什么样的心情?与此相对照的是,我们又希望自己对别人直来直去的时候,别人会高兴的接纳,这可真是奇怪的事情。

    (3) 网上“直率”,现实中彬彬有礼

    有些人在网上非常的“直率”,直率到了一不高兴随时骂娘的地步,骂了又怎么样,反正见不着你。我相信在现实生活中,他们大部分都是讲道理有礼节之人,因为在现实中,我从来没有碰到像网上那么多人动不动就爆粗口、骂娘,祖宗十八代什么都可以骂。这样的人,有人格分裂倾向。一个人格完整健全的人,应该是一个不管什么场合都言行一致的人。

    (4)表面直率,心里打算盘

    在中国什么类型电视剧最热?我想无非是武侠剧、宫廷剧、历史剧,这些电视剧无一不充斥着人与人之间勾心斗角的故事。有一位伟人说:“与人斗,其乐无穷”,试想一个什么都藏不住的人,与人斗,还不被别人玩死啊?因此,无耻的政客们不论心里面打着多坏的算盘,表面上也得笑着,一副给人家交了老底了样子,好叫别人不要防着。当然,职场中还没有这么夸张,那是因为我们没有那大的利益需要去争斗,但中国人长期受这种文化的熏陶,表里不一的人到处都是。

    上面这些所谓的“悖论”只为了引起大家的思考,对于直率是好是坏这样的问题,并没有标准答案。但凡事过犹不及,过于直率无疑不但会伤害别人,最后也会反过来伤害到自己。

    3.直来直去伤人害己

    直率这种性格听上去还不错,显得一个人光明正大,无所畏惧。有些人在自我介绍时候会说:“我这个人就是个直肠子”,言语之间还透着一些小小的得意。

    很多人将直率视为一种美德。美德应当是对别人对自己都有好处的事情,可是“直率”并不是这样,如果把握不了度,可能反而会伤人害己而不自知。

    (1)伤人

    要说直率伤人,相信人人都懂,很多人还会有切身的体会。国人最讲面子,一旦面子被伤,很难挽回,正所谓“刀伤易痊,舌伤难愈”。武侠小说中江湖中人,为什么整天打打杀杀的?直率就是一个重要的原因,那些人个个都是直率之人,又不讲礼节,经常出言不逊,一言不合,就兵刃相见。好在祖先教我们注意礼节,用礼节约束我们的言语和行为,社会就会和谐多了。

    有一次我收到一个程序员的辞职申请,让我奇怪的是,这名程序员一向工作踏实,为什么突然辞职呢?跟他谈过之后,这才明白原因很简单,就是因为项目经理多次批评他“怎么这么简单的事情都做不好”,而他认为事情并不简单,是项目经理不了解实际情况。但他不想解释,因为他的自尊心被伤害了,不想再待在这里工作。我又找项目经理沟通,项目经理说这是无心之言,只怪现在的员工太脆弱。最后的结果就是员工离职了,项目进度也受到一定的影响。试想,如果项目经理在说话的时候更加注意措辞,怎么会有这样的结果呢?

    (2)害己

    还记得《三国演义》中的杨修是怎么死的吗?杨修有过人之才,总是能看穿曹操的意图,然后得意洋洋的告诉别人,最后被曹操以扰乱军心的罪名处死。在历史上,许许多多的忠臣最后都没有好下场,因为他们往往是刚直之辈,言语伤了领导的面子,而领导一旦报复,后果是很可怕的。

    直率就像没有成熟的柿子一样,好看不好吃。现实中,只听说过因为直率吃大亏,没有听说过谁因为直率升职加薪,不是这样的吗?一个管不住自己嘴巴的人,小则在团队中难以与人和谐相处,大则可能会得罪客户、甚至泄露公司机密,这样的人,领导怎么敢委以重任呢?

    一个人能力很强,却因为说话的问题而吃亏,那实在不划算。技术的成长需要日积月累,而一句未经思考的话就有可能毁掉你在公司的前程。因此在把话说出去之前,我们应该多思考,三思而后言,避免犯下言语上的低级错误,后悔莫及。

    4.三思而后言

    在计算机中,我们常用IPO(输入-处理-输出)图来描述一个过程或一个功能模块的设计,其实人说话也是一个输入-处理-输出的过程。首先我们接收到信息,比如别人说的话、文件、任务指令或其它外部事件,然后大脑对这些信息进行处理、思考,决定要说什么,最后嘴巴将大脑思考的结果输出,也就是说话了。

    人与人之间说话的过程,主要区别就在于大脑思考处理这个环节。过于直率从某种程度来说,是一种有失理智的行为,因为他没有经过“理智”的思考,全凭个人的直觉反应来应对外部事件。

    (1) 两种说话模型

    根据人们说话的“输入-处理-输出”过程的不同,可以将说话的分为直率型和谨慎型两种方式。

    对于直率型的说话方式,其主要特征是思考问题的方式比较简单,只是根据大脑的直觉做出反应,得到结论,然后直截了当的将结论输出(说出来),而且往往是一个输入对就对应一个输出,整个过程看上去就像一根“直肠子”,如下图所示:

    图1 直率型说话模型

    这个图让人不禁想起了巴浦洛夫的条件反射理论,显然直率型的人在说话方面没有充分利用人的思维能力,发挥人的主观能动性。

    而谨慎型的人,他们的思考过程比较复杂,在直觉反应之后,还要经过大脑的分析总结,在说话之前要判断是否可以说,因为除了说之外,他们还可以选择沉默。另外与直率型不同的是,他们往往多个输入对应一个输出,这意味着他们说之前要听对方把话说完,而不是匆匆忙忙下结论。

    图2 谨慎型说话模型

    两种方式比较如下表所示:

    环节

    直率型

    谨慎型

    说明

    点评

    说明

    点评

    输入

    每个输入一个输出

    快人快语

    多个相关输入一起处理

    把话听完,察言观色,结合事件背景,边听边看边想

    处理

    直觉反应→结论,缺乏思考与分析的过程

    说话不经过大脑

    多了思考分析的过程,同时话说还有判断是否可说

    说前要思考,嘴巴多了一个把门的

    输出

    每个输入一个输出

    直肠子

    多个输入,一个输出(必要时沉默)

    心有城府

    (2)三思而后言

    从上面的模型可以看到,谨慎型的人主要特征是说话理智,真正做到了三思而后言。说话一定要经考大脑思考,没有思考,我们就如同一个被自己的性格的直觉控制的牵线木偶。因此,当我们与人交流时,务必要时时思考说什么以及怎么说的问题。

    ● 说什么

    说什么就是要判断我们想说的内容可不可以说。说话不能逞一时之快,千万不可说极端的话。还有就是要学会察言观色,学会从事件的背景、对方面的性格、脸色、手势等“输入”信息来揣测对方意图,从而做出合适的反应。

    ●怎么说

    这是说话方式的问题。要注意对事不对人,看透不说透,立场要客观,不能偏激,好话可以直接说,不好的话要委婉的说。其实程序员直率一点,尚无大碍,因为程序员最重要的事情就是高效地写出合格的代码,即使你伤害了你和经理,他一般也不会计较;但对项目经理而言,沟通是其最重要的工作,他需要与各方、代表不同利益的人打交道,如果想到什么说什么,不但会被人家认为肤浅,而且你的底牌、你的真实想法全在人家的掌握之中,无论是在处理内部冲突的时候,还是与客户谈判的时候,都会陷入被动的境地。“见人说人话,见鬼说鬼话”,这话虽然“逆耳”,但是“利于行”啊。

    养成思考的习惯,刚开始可能会很不适应,觉得很累,但只要坚持,久而久之,就会习惯成自然,用大脑管住嘴巴。

    当然慎言也不能过度,否则可能会造成胆怯,什么都不敢说,反而不利于问题的解决。把握好度,不要走向反面,好与坏之间往往只隔着一层纸。

    需要说明的是,本人也绝不不是什么说话的高手,我对自己的要求不高,就是尽量照顾别人的感受,不捅漏子!当然即使是这个看上去简单的要求,也必须要改掉过于直率的毛病,凡事三思而后言!

    5.守住真我

    (1)直率也有可取之处

    三思而后言,并不是要将直率一棒子打死。之所以要避免过于直率,是为了避免伤害别人和自己受伤,而不是要把自己藏得很深以进行利益的争斗,更不是要去算计别人,记住这个出发点。

    因此该直率的时候还是要直率,否则就容易变成虚伪。比如在讨论技术问题时,就应该畅所欲言,让别人准确了解你的想法,注意就事论事、不搞人身攻击就可以了。在分配工作时也是这样,应该力求清晰明确。再比如,项目遭遇危机的时候,有什么想法要全盘说出来,就算只是头脑风暴也有价值。如果这时候还在考虑要直率还是委婉的说,那就好比一个人都快饿晕了,还在考虑是要吃米饭还是面包一样。在生活中,也有需要直率处理的问题,比如谈恋爱,如果总是含蓄,始终不感说出“我爱你”三个字,说不定你的心上人就要飞了。

    (2) 真我永存

    周星驰在《喜剧之王》中有一句台词:“其实我并不是一个直率的人,我只是个演员!”说出这句话,多少有些无奈,谁不想过恣意纵横的生活呢?每个人都是生活这个舞台上的演员,需要涂脂抹粉,甚至要戴上面具,不然容易伤到别人,或为别人所伤。但这并不意味着要泯灭真我,因为真我就如“挪威的森林”一样,别人无法触碰得到,但永远存在于我们的内心深处。

    有人说,直率的人生活更加真实、更有个性,如果人人都像演员,便那岂不人人都失了性格?其实性格是一种内在的秉性,并不会因为你管住了自己的嘴巴就没有了性格,难道含蓄和委婉的人就没有了性格吗?我们说话做事应该以原则为导向,而不是被性格所牵引和控制,只要你内心有自己的原则,你就是一个性格的人。

    不要过于直率,但也不要走向了另外一个极端。如果心机太深,或者虚情假意,那就变成了虚伪和狡诈,没有人会愿意与这样的人交往,因为他们的行为是不友好的,已经脱离了善的范畴。只要心存善念,真我就会永存。

    从程序员到项目经理(九):程序员加油站 — 再牛也要合群
    “丛林法则”从未离我们远去,“适者生存”仍然是支配社会运行的一般法则。对于一群社会性动物而言,所谓“适者”,不只是体格的强壮,更重要的是能参与群体的公共生活。即使是最强大的狮子,只要离群,也只有死路一条!

    1.好汉也要三个帮

    我喜欢看动物世界,感受那些发生在非洲大草原上的那些美丽或者哀伤的故事。那里生活着成群的狮子和鬣狗、还有数以百万计的野牛和角马。无论是凶猛的狮子,还是温驯的角马,都属于是群居动物,个体一旦离群,就会离死亡不远了。

    其实人也是一样。人是一种社会性动物,我们只能生活在社会群体中,离开了群体,我们的人生价值也就无所依附。在社会心理学名著《社会性动物》的扉页上,印着一段亚里士多德的名言:“从本质上讲人是一种社会性动物;那些生来离群索居的个体,要么不值得我们关注,要么不是人类。社会从本质上看是先于个体而存在的。那些不能过公共生活,或者可以自给自足不需要过公共生活,因而不参与社会的,要么是禽兽,要么是上帝。”

    其实这段话应该修正一下,许多动物也是要过公共生活的,至于上帝,我们都不曾见过,想必也是差不多的。无论是希腊神话中的宙斯,还是中国神话中的玉皇大帝,他们身边不也是都有一班大小天神簇拥左右吗?

    可见下至动物、上至上帝都需要合群,更何况是人?

    可是在程序员这样一个群体中,确实还是有不少人不喜欢与别人打交道,喜欢独来独往,过着自我封闭、离群索居的生活。

    一个人不合群的原因有很多种,比如:价值观不一致、胆小害羞、不善言辞、性格内向等。而对于一个技术牛人来说,其不合群的原因还要加上一条:看不起别人,觉得“竖子不足与谋”。

    中国素来有文人相轻的习惯,其实程序员相轻的现象一点也不比文坛少。程序员多有自傲的性格,容易看高自己,看扁别人。觉得自己一个人也能搞定所有事情,多几个人来弄反倒碍手碍脚 。

    当今社会是一个高度分工、讲求合作的社会,每个人都是团队中的一员,总想着个人单干的小农思想,已经无法与现实相容。个人英雄主义的时代已经远去,在一个项目中更是如此。俗话说:“一个好汉要三个帮”,一个人再牛,也应该学会欣赏别人的优点、与人和睦相处,因为没有这“三个帮”,他便当不成英雄好汉,空有一身武功,四处碰壁,一事无成。

    2.合群谁都可以做得到

    每个人都内心里对外在的事物都有一道防线,这是一种自我保护的本能。对于不合群中的人,这道防线显得格外的高大和坚固,以至于将他与其他人隔离成了两个世界。其实合群并不是一件难事,关键是要敞开心扉,卸掉内心的防线,主动与别人交往,融入到所在的团队中。当然,合群也需要注意一些问题,避免盲目交往,或者言行失范。

    (1)合什么样的群

    合什么样的群,也就是说我们应该与什么样的人交往。所谓“近朱者赤,近墨者黑”,因此与有必要对自己交往的对象加以界定。

    如果是一帮举止不端或格调不高的人,应该果断退出,平时也应保持适当距离,以不得对方为限。

    对于自己不感兴趣或者对自己助益较少的群体,不要一概拒绝,否则会给人以不近人情的印象。可以适当参与他们的活动,但不能过多,否则会占用自己过太多时间。

    交往的重点应该是与自己兴趣相投、对自己有帮助的人。与他们相处,不但可以互相学习,而且人生的快乐和价值可以找到落脚点。

    (2)言行的把握

    在与人交往中,言行得体是非常重要的。2009年河南有个局长叫逯军,因为一句“你是准备替党说话,还是准备替老百姓说话”名扬天下,沦为笑柄。 最近,“表叔”杨达才因为在车祸现场诡异一笑,不但引得丢官弃爵,恐怕还要陷入牢狱之灾!

    在我们普通人的生活中,因为言行不慎,招来误解、怨恨的例子同样非常多。

    对于言行的把握最重要的是要谦和、通融、合规、适度。例如大家玩的时候你也玩,不要做异类;开玩笑不要过分、让人难堪;举止不要怪异等。

    (3)尊重他人,保持平等

    这是对牛人的忠告,因为牛人技能超群,更容易觉得自己高人一等,看不起别人。人与人交往最重要的是获得尊重和认同,如果他不能从你这里获得这些,你就是比牛顿还牛,对他而言也是没有价值的。须知,尊重是双向的,合群的首要点便是尊重对方,以平等之心相待,不卑不亢,这样才能赢得别人的尊重与认同。

    从程序员到项目经理(十):程序员加油站 –要执着但不要固执
    程序员的成长之路,没有捷径可走,只有坚持不懈的执着追求,才能成为一名优秀的程序员。执着诚然可贵,但如果不能经常自省,则有可能会陷入固执的境地。

    1.程序员需要一点执着精神

    《士兵突击》中许三多有一句名言:“不抛弃、不放弃”,这是一种可贵的执着精神。正是靠着这种不抛弃、不放弃的执着追求,许三多从一个普通的小兵,成长为团部的精英。在现实生活中也是这样,可以说大凡取得一定成就的人,在工作中都是一个执着的人。

    对程序员则言,执着精神尤为可贵。在编程过程中,我们难免会碰到各种问题,如果没有一点执着精神,一碰到问题就抱怨、回避,怎么可能取得技术上的突破呢?又怎么能体会到解决问题的快感呢?

    回想起我刚入门学习GIS(地理信息系统)编程时,经理就给我安排了一个之前让不少人望而却步的难题,用MapObjects实现地图符号化,要求具有自定义符号库的功能。以我当时的经验,根本不知道从何下手,但也只能硬着头皮上。首先我把MapObjects的帮助文件全部仔仔细细看了一遍后,找到一个CustomDraw接口。但是只是一个接口而已,离完整的符号化功能还相差很远。怎样利用这个接口呢?当时网络还很落后,网上的编程资料更少,关于MapObjects的中文开发资料则几乎没有,于是我又通过蜗牛速度的网络,查阅国外的相关英文资料,在片言只语中寻求灵感。那一段时间我无论是吃饭、睡觉,还是走了路上,无时无刻不在思考技术上的问题,由于坚持不懈的努力,我一次次获得小小的启发,一步步接近问题的解决之道。6个月艰苦摸索之后,我终于彻底搞定了这个在公司内公认的难题,我本人也从一个门外汉,一举成为了公司的核心技术人员。这一段时间,我不但把MapObjects每个接口弄得烂熟,还学会了一百多个Windows API的使用,无论是技术方面,还是个人的职业生涯,都取得了一次飞跃。

    程序员都需要一些执着的精神,来磨炼自己、发展自己,要有水滴石穿的决心和勇气,才能够成为真正优秀的程序员。

    2.自省消除固执

    固执和执着一样,都是一种坚持不放弃的精神,既然如此,那为什么人们总是赞美执着的人,对固执却嗤之以鼻呢?

    其实两者的差别全在于坚持的方向。执着和固执,就像一根绳子的两端,虽然是在同一根绳子上,方向却相反。执着是沿着正确的方向前进,是一种理智的坚持,而固执则恰好相反。既然都是坚持,那怎么判断方向是否正确呢?

    其实,何为正确,何为错误,两者之间并不是泾渭分明,不然,也就不会有那么多“执迷不悟”的人了。方向是否正确,往往是以结果来衡量的。因此是执着还是固执,其实主要是结果导向,结果好就是执着,结果不好,就是固执。爱迪生发明灯泡的时候,经历了无数次的失败仍然坚持不懈,最后终于找到了用钨丝作为灯丝方法,取得了成功,他的坚持我们称之为执着。后来,爱迪生创立了通用电气公司,坚持用直流电供电,无视交流电在远距离传输方向的巨大优势,最后输给了采用交流电方案的西屋电气公司,他自己也只黯淡离开自己创立的公司,这时候,我们只能说发明大王也有固执的时候。

    如此说来,难道我们非要等要结果发生,才能知道自己的坚持是对是错吗?有没有办法让我们在进行过程中就能出判断呢?这只能靠我们的自省。孔子曰:“吾日三省吾身”,大凡善于自省的人,都不会是固执的人。他们能随时察觉自身的问题,具有理智的否定自己的勇气。

    自省需要常识。对于一个不具备常识、不明白对错、不理解基本规则的人,怎么能正确判断方向呢?这样的人再怎么自省也是无济于事的,他只有在不断的碰壁中才能获得真正的成长。

    我曾经见到一些程序员,在自己的想法与项目经理发生冲突时,总是一味的坚持,不肯让步,甚至与项目经理陷入无休止的争吵,还以为自己掌握了真理。殊不知,与上司顶撞是一种愚蠢的行为,这种过分的坚持,会在上司心目中形成不听话的印象。更何况,服从上级工作安排是基本的职场规则,你可以提意见,但必须尊重上司的决定。毫无疑问,在这场对峙中,不管理项目经理对错,程序员都是固执的一方。如果程序员具备这些基本的常识,并且保持自省,也就不会发生这样的事情了。

    自省还需要具有突破思维舒适区的勇气。每个人的都有其思维舒适区,这里一切受潜意识的保护,一切都似乎理所当然,我们的大脑无需对事物做过多的思考,爽爽的享受这种自我封闭带来的轻松和愉悦。毫无疑问,思维舒适区阻挡了我们对事物深层次的探求,以及我们对不同观点的接纳,因而也就无法对自己所坚持的东西做出真正客观的分析。

    在程序员与项目经理的争吵中,其实双方都应该勇敢跳出自己的舒适区,心平气和地考虑,对方的观点是否也具有可以接纳的成分,做一个理智的坚持者,这样才能做到双赢。执着还是固执,往往也就只是在一念之间的差别。

    从程序员到项目经理(十一):每个人都是管理者
    从程序员转为项目经理,这是一个巨大的跨越。一个新任的项目经理,对项目管理找不到感觉,一般也被认为是一件正常的事情。这是否意味着,一定要等到当上了项目经理才能学习项目管理吗?一定要做砸一个项目才能成长为合格的项目经理吗?其实未必,项目管理所需要素质和技能并不是什么独门秘籍,而是在生活中时时用到、处处可以锻炼的。只要有心,程序员一样可以学习和实践项目管理知识。从某种程度来说,我们每个人都是管理者。

    1.管理是职能而不是职位

    管理学之父彼德.德鲁克曾说:“任何一位做决策的人,其工作性质和董事长,和行政领导相同。即使他的管辖范围有限,甚至于他的职能或他的大名,不见于组织系统里,办公室连专线电话也没有,但他确实也是一位管理者。”

    可见管理并不是经理、老总的专权,管理不是个职位,而是个职能。无论你在什么岗位,也不论你有没有下属,只要你需要做出决策,需要对结果负责,那你就是个管理者。从这个角度来说,我们每个人都是管理者,因为每个人都需要对自己的生活的工作负责,对碰到问题进行权衡决策,只不过决策的内容不一样而已。

    程序员显然也需要对工作进行决策。当接受任务时,程序员需要对工作量、工作难度、时间限制进行评估,以确定能否实现项目经理的目标;开发一个功能点时,我们需要思考哪些实现方式,哪种方式开发速度、运行效率、对资源的占用几个方面综合最优;最进度滞后时,是要加班赶回来,还是