Android源码系列(13) -- ArrayMap

栏目: Android · 发布时间: 6年前

内容简介:ArrayMap是通用的键值对映射数据结构,相比传统的HashMap有更高的内存利用率。使用数组的数据结构保存映射:一个整形数组存放每项的哈希值,一个Object数组存放键值对。通过使用数组,能避免为每个放入Map的entry创建额外的对象(对比HashMap的Node),并能更有力地控制数组扩展的大小(扩增数组只需要创建一个新的数组并拷贝元素,不是重建hash map)。此数据结构不适合用在存放大量items的场景。相比传统HashMap,ArrayMap速度要更慢。因为ArrayMap通过二分查找搜索

一、类签名

ArrayMap是通用的键值对映射数据结构,相比传统的HashMap有更高的内存利用率。使用数组的数据结构保存映射:一个整形数组存放每项的哈希值,一个Object数组存放键值对。通过使用数组,能避免为每个放入Map的entry创建额外的对象(对比HashMap的Node),并能更有力地控制数组扩展的大小(扩增数组只需要创建一个新的数组并拷贝元素,不是重建hash map)。

public final class ArrayMap<K, V> implements Map<K, V>

此数据结构不适合用在存放大量items的场景。相比传统HashMap,ArrayMap速度要更慢。因为ArrayMap通过二分查找搜索item,添加或移除元素也需要操作数组的entries。不过容器仅持有的数百个items时,两者的性能差别并不明显,差别不超过50%。

Android源码系列(13) -- ArrayMap

为了更平衡的内存利用率,ArrayMap不像其他的标准 Java 容器类,而是会在items移除的同时缩小数组。现在也不能主动控制数组的缩小,也就是说,设置了capacity,当任意item被移除,数组的大小都会减少到刚好容纳所有item的大小。

源码来自Android 26

二、常量

private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;

// 最低扩容容量
private static final int BASE_SIZE = 4;

// cache size大小
private static final int CACHE_SIZE = 10;

// 特殊的哈希数组值,用来表示container不可变
static final int[] EMPTY_IMMUTABLE_INTS = new int[0];

// 特殊的不可变空ArrayMap常量,主要用于表示空状态
public static final ArrayMap EMPTY = new ArrayMap<>(-1);

三、成员变量

// 缓存小数组对象避免垃圾回收
 // mBaseCache[0]指向列表中的下一个数组
 // mBaseCache[1]指向int[]
static Object[] mBaseCache;
static int mBaseCacheSize;

 // 缓存小数组对象避免垃圾回收
static Object[] mTwiceBaseCache;
static int mTwiceBaseCacheSize;

final boolean mIdentityHashCode;

// 哈希数组
int[] mHashes;

// 存放key-value的数组
Object[] mArray;

// 已保存items数量
int mSize;

MapCollections<K, V> mCollections;

四、构造方法

// 构造空ArrayMap,初始默认容量为0
public ArrayMap() {
    this(0, false);
}

// 根据给定的容量值构造实例
public ArrayMap(int capacity) {
    this(capacity, false);
}

public ArrayMap(int capacity, boolean identityHashCode) {
    mIdentityHashCode = identityHashCode;

    if (capacity < 0) {
        // 此实例内容是不可变的,使用常量标记
        mHashes = EMPTY_IMMUTABLE_INTS;
        mArray = EmptyArray.OBJECT;
    } else if (capacity == 0) {
        // 此实例内容可变,但初始大小为0
        mHashes = EmptyArray.INT;
        mArray = EmptyArray.OBJECT;
    } else {
        // 根据指定大小进行构造
        allocArrays(capacity);
    }
    // 以保存元素数量为0
    mSize = 0;
}

// 根据给定的map构造实例
public ArrayMap(ArrayMap<K, V> map) {
    this();
    if (map != null) {
        putAll(map);
    }
}

五、静态方法

// 二分查找
private static int binarySearchHashes(int[] hashes, int N, int hash) {
    try {
        return ContainerHelpers.binarySearch(hashes, N, hash);
    } catch (ArrayIndexOutOfBoundsException e) {
        if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
            throw new ConcurrentModificationException();
        } else {
            throw e; // the cache is poisoned at this point, there's not much we can do
        }
    }
}

// 释放hash数组与key-value数组
// 合适的数组不会释放而是缓存起来,下次数组大小合适时复用
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    // hashes.length == 8
    if (hashes.length == (BASE_SIZE*2)) {
        synchronized (ArrayMap.class) {
            // mTwiceBaseCacheSize最多保存10个
            if (mTwiceBaseCacheSize < CACHE_SIZE) {
                // 通过链表的方法保存
                array[0] = mTwiceBaseCache;
                array[1] = hashes;
                // 置空数组索引[2,(size<<1)-1]的entry
                for (int i=(size<<1)-1; i>=2; i--) {
                    array[i] = null;
                }
                mTwiceBaseCache = array;
                mTwiceBaseCacheSize++; // 已保存数量递增
            }
        }
    } else if (hashes.length == BASE_SIZE) { // hashes.length == 4
        synchronized (ArrayMap.class) {
            // mBaseCacheSize最多保存10个
            if (mBaseCacheSize < CACHE_SIZE) {
                array[0] = mBaseCache; // index = 0
                array[1] = hashes;     // index = 1
                // 置空数组index [2,(size<<1)-1]的entry
                for (int i=(size<<1)-1; i>=2; i--) {
                    array[i] = null;
                }
                mBaseCache = array;
                mBaseCacheSize++; // 已保存数量递增
            }
        }
    }
}

六、成员方法

// 通过key和hash找索引
int indexOf(Object key, int hash) {
    final int N = mSize;

    // ArrayMap为空,返回~0
    if (N == 0) {
        return ~0;
    }

    // 通过二分查找确认索引值
    int index = binarySearchHashes(mHashes, N, hash);

    // 如果找不到哈希码,表示此key没有对应的Entry
    if (index < 0) {
        return index;
    }

    // 该key和index所表示的'key'匹配,则表示index成功命中
    if (key.equals(mArray[index<<1])) {
        return index;
    }

    // 从index开始,检查后方索引的key是否匹配
    int end;
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {
        if (key.equals(mArray[end << 1])) return end; // 返回匹配的索引值
    }

    // 从index开始,检查前方索引的key是否匹配
    for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
        if (key.equals(mArray[i << 1])) return i; // 返回匹配的索引值
    }

    // Key not found -- return negative value indicating where a
    // new entry for this key should go.  We use the end of the
    // hash chain to reduce the number of array entries that will
    // need to be copied when inserting.
    return ~end;
}

int indexOfNull() {
    final int N = mSize;

    // ArrayMap为空,返回~0
    if (N == 0) {
        return ~0;
    }

    int index = binarySearchHashes(mHashes, N, 0);

    // If the hash code wasn't found, then we have no entry for this key.
    // 哈希值没有命中,表示此kay没有对应的entry.
    if (index < 0) {
        return index;
    }

    // If the key at the returned index matches, that's what we want.
    // 该key和index所表示的'key'匹配,则表示index成功命中
    if (null == mArray[index<<1]) {
        return index;
    }

    // 从index开始,检查后方索引的key是否匹配
    int end;
    for (end = index + 1; end < N && mHashes[end] == 0; end++) {
        if (null == mArray[end << 1]) return end;
    }

    // 从index开始,检查前方索引的key是否匹配
    for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
        if (null == mArray[i << 1]) return i;
    }

    // Key not found -- return negative value indicating where a
    // new entry for this key should go.  We use the end of the
    // hash chain to reduce the number of array entries that will
    // need to be copied when inserting.
    return ~end;
}

// 分配数组空间
private void allocArrays(final int size) {
    // 若mHashes为EMPTY_IMMUTABLE_INTS,表示此ArrayMap是不可变的,并抛出异常
    if (mHashes == EMPTY_IMMUTABLE_INTS) {
        throw new UnsupportedOperationException("ArrayMap is immutable");
    }
    // size == 8,从缓存中复用数组
    if (size == (BASE_SIZE*2)) {
        synchronized (ArrayMap.class) {
            if (mTwiceBaseCache != null) {
                final Object[] array = mTwiceBaseCache;
                mArray = array;
                // mTwiceBaseCache = mTwiceBaseCache[0]
                mTwiceBaseCache = (Object[])array[0];
                // 取出缓存的hashes赋值给mHashes复用
                mHashes = (int[])array[1];
                // 置空索引0和索引1的空间
                array[0] = array[1] = null;
                // 已缓存数组数量递减
                mTwiceBaseCacheSize--;
                return;
            }
        }
    } else if (size == BASE_SIZE) { // size == 4,从缓存中复用数组
        synchronized (ArrayMap.class) {
            if (mBaseCache != null) {
                final Object[] array = mBaseCache;
                mArray = array;
                // mBaseCache = mBaseCache[0]
                mBaseCache = (Object[])array[0];
                // 取出缓存的hashes赋值给mHashes复用
                mHashes = (int[])array[1];
                // 置空索引0和索引1的空间
                array[0] = array[1] = null;
                // 已缓存数组数量递减
                mBaseCacheSize--;         
                return;
            }
        }
    }
    
    // size大小不是4或8,则就地创建新数组并赋值
    mHashes = new int[size];
    mArray = new Object[size<<1];
}

// 清空所有键值对数据
@Override
public void clear() {
    // 以保存元素数量不为空
    if (mSize > 0) {
        // ohashes i.e oldHashes
        final int[] ohashes = mHashes;
        // oarray i.e oldArray 
        final Object[] oarray = mArray;
        // osize i.e oldSize
        final int osize = mSize;
        // 设置EmptyArray.INT表明为空
        mHashes = EmptyArray.INT;
        // 设置EmptyArray.OBJECT表明为空
        mArray = EmptyArray.OBJECT;
        // 已保存item为0
        mSize = 0;
        // 释放数组空间
        freeArrays(ohashes, oarray, osize);
    }
    // 检查是否存在并发修改
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) {
        throw new ConcurrentModificationException();
    }
}

// 清除所有items,但不减少ArrayMap已开辟空间
public void erase() {
    if (mSize > 0) {
        final int N = mSize<<1;
        final Object[] array = mArray;
        for (int i=0; i<N; i++) {
            array[i] = null;
        }
        mSize = 0;
    }
}

// 保证ArrayMap扩容到minimumCapacity个空间
public void ensureCapacity(int minimumCapacity) {
    final int osize = mSize;
    if (mHashes.length < minimumCapacity) {
        final int[] ohashes = mHashes;
        final Object[] oarray = mArray;
        // 创建新数组空间
        allocArrays(minimumCapacity);
        // 迁移items到新数组空间
        if (mSize > 0) {
            System.arraycopy(ohashes, 0, mHashes, 0, osize);
            System.arraycopy(oarray, 0, mArray, 0, osize<<1);
        }
        // 释放旧数组空间
        freeArrays(ohashes, oarray, osize);
    }
    // 检查是否存在并发修改
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
        throw new ConcurrentModificationException();
    }
}

// 检查key是否包含在数组中
@Override
public boolean containsKey(Object key) {
    return indexOfKey(key) >= 0;
}

 // 返回key在集合中的索引
public int indexOfKey(Object key) {
    return key == null ? indexOfNull()
            : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
}

// 检查value在数组中的索引值
int indexOfValue(Object value) {
    final int N = mSize*2;
    final Object[] array = mArray;
    if (value == null) {
        // value为空,则查找第一个值为空的索引
        for (int i=1; i<N; i+=2) { // 每次递增2,保证索引指到值的位置上
            // array[i]匹配null
            if (array[i] == null) {
                return i>>1; // 整形值通过右移1位完成除以2的操作,即i/2
            }
        }
    } else {
        // value不为空,则查找第一个值匹配的索引
        for (int i=1; i<N; i+=2) { // 每次递增2,保证索引指到值的位置上
            // array[i]value
            if (value.equals(array[i])) {
                return i>>1; //  整形值通过右移1位完成除以2的操作,即i/2
            }
        }
    }
    // 集合中没有指定key,返回-1
    return -1;
}

// 检查value是否包含在数组中,此调用会对整个数组进行线性查找
@Override
public boolean containsValue(Object value) {
    return indexOfValue(value) >= 0;
}

// 根据key从数组中获取对应值,没有对应值则返回null
@Override
public V get(Object key) {
    // 根据key算出所在数组的索引值
    final int index = indexOfKey(key);
    // 从索引值取值,若索引值为负数,则返回null
    return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}

// 根据key的索引从数组中取key的实例
public K keyAt(int index) {
    // 根据索引值直接取key
    return (K)mArray[index << 1];
}

// 根据value的索引从数组中取value的实例
public V valueAt(int index) {
    // 根据索引值直接取value
    return (V)mArray[(index << 1) + 1];
}

// 根据key的索引在数组中设置key的新值,如果此索引已有旧值,则作为结果返回
public V setValueAt(int index, V value) {
    index = (index << 1) + 1;
    // 获取旧元素
    V old = (V)mArray[index];
    // 存入新元素
    mArray[index] = value;
    // 返回旧元素
    return old;
}

// 检查数组是否为空
@Override
public boolean isEmpty() {
    return mSize <= 0;
}

// 向ArrayMap中存入新的<Key, value>,如果key有对应的旧value,则旧value会被替换
@Override
public V put(K key, V value) {
    // 原数组元素数量
    final int osize = mSize;
    // 根据变量key算出来的哈希值
    final int hash;
    // 索引值
    int index;
    // key为空则搜索值为空的索引
    if (key == null) {
        hash = 0;
        index = indexOfNull();
    } else {
        // 通过key计算索引
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        // key和hash查找索引
        index = indexOf(key, hash);
    }
    if (index >= 0) { // 命中已有item
        // 已有key在mArray数组的索引
        index = (index<<1) + 1;
        // 取出旧值
        final V old = (V)mArray[index];
        // 数组相同位置存入新值
        mArray[index] = value;
        // 返回旧值
        return old;
    }

    index = ~index;
    if (osize >= mHashes.length) {
        // osize >= BASE_SIZE*2 的大小为 osize+(osize>>1)
        // BASE_SIZE*2 > osize >= BASE_SIZE 的大小为 BASE_SIZE*2
        // osize < BASE_SIZE 的大小为 BASE_SIZE
        final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

        final int[] ohashes = mHashes;
        final Object[] oarray = mArray;
        allocArrays(n);

        // 检查是否存在并发修改
        if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
            throw new ConcurrentModificationException();
        }
        
        // ohashes元素全部复制到mHashes
        // oarray元素全部复制到mArray
        if (mHashes.length > 0) {
            System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
            System.arraycopy(oarray, 0, mArray, 0, oarray.length);
        }
        
        // 释放ohashes和oarray的空间
        freeArrays(ohashes, oarray, osize);
    }

    // 新存入key-value需插入到原有元素之间
    if (index < osize) {
        System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
        System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
    }

    // 检查是否存在并发修改
    if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
        if (osize != mSize || index >= mHashes.length) {
            throw new ConcurrentModificationException();
        }
    }
    // 数组已创建完毕,且原有元素位置都移动好,则在指定位置放入新key-value
    mHashes[index] = hash;
    mArray[index<<1] = key;
    mArray[(index<<1)+1] = value;
    // 以保存元素数量递增
    mSize++;
    return null;
}

// 不验证数组是否有足够空间插入key-value
public void append(K key, V value) {
    int index = mSize;
    final int hash = key == null ? 0
            : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
    if (index >= mHashes.length) {
        throw new IllegalStateException("Array is full");
    }
    if (index > 0 && mHashes[index-1] > hash) {
        RuntimeException e = new RuntimeException("here");
        e.fillInStackTrace();
        put(key, value);
        return;
    }
    // 空间足够,正常插入key-value
    mSize = index+1;
    // 存入哈希值
    mHashes[index] = hash;
    index <<= 1;
    // 存入key-value
    mArray[index] = key;
    mArray[index+1] = value;
}

// 使用append()会导致array maps失效,尤其是同一个key出现了多次
// 此方法验证array map是有效的,并在出现问题时抛出IllegalArgumentException
public void validate() {
    final int N = mSize;
    if (N <= 1) {
        // There can't be dups.
        return;
    }
    int basehash = mHashes[0];
    int basei = 0;
    for (int i=1; i<N; i++) {
        int hash = mHashes[i];
        if (hash != basehash) {
            basehash = hash;
            basei = i;
            continue;
        }

        // 向前检查看key是否出现重复
        final Object cur = mArray[i<<1];
        for (int j=i-1; j>=basei; j--) {
            final Object prev = mArray[j<<1];
            if (cur == prev) {
                throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
            }
            if (cur != null && prev != null && cur.equals(prev)) {
                throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
            }
        }
    }
}

// 把array变量中的所有键值对存入实例中
public void putAll(ArrayMap<? extends K, ? extends V> array) {
    // 获取当前实例array长度
    final int N = array.mSize;
    // 确保当前ArrayMap有足够空间保存array的所有元素
    ensureCapacity(mSize + N);
    if (mSize == 0) {
        if (N > 0) {
            System.arraycopy(array.mHashes, 0, mHashes, 0, N);
            System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
            mSize = N;
        }
    } else {
        for (int i=0; i<N; i++) {
            put(array.keyAt(i), array.valueAt(i));
        }
    }
}

 // 从ArrayMap中移除该存在的key和对应的value
@Override
public V remove(Object key) {
    // 根据key查找对应索引
    final int index = indexOfKey(key);
    // 索引值大于0表示key存在
    if (index >= 0) {
        // 移除该key
        return removeAt(index);
    }
    
    // key没有找到,返回null
    return null;
}

// 根据给定的index移除目标键值对,方法结果返回键值对的值
public V removeAt(int index) {
    final Object old = mArray[(index << 1) + 1];
    final int osize = mSize;
    final int nsize;
    if (osize <= 1) {
        // Now empty.
        freeArrays(mHashes, mArray, osize);
        mHashes = EmptyArray.INT;
        mArray = EmptyArray.OBJECT;
        nsize = 0;
    } else {
        nsize = osize - 1;
        // mHashes.length > 8 && mSize < mHashes.length/3
        if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
            // 缩小数组的空间,并且大小不低于BASE_SIZE*2
            final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);

            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n);
            
            // 出现并发修改,抛出异常
            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
                throw new ConcurrentModificationException();
            }
            
            if (index > 0) {
                // 从ohashes拷贝[0, index)到mHashes
                System.arraycopy(ohashes, 0, mHashes, 0, index);
                // 从oarray拷贝[0,index << 1)到mArray
                System.arraycopy(oarray, 0, mArray, 0, index << 1);
            }
            if (index < nsize) {
                // 从ohashes拷贝剩余items到mHashes
                System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                // 从oarray拷贝剩余items到mArray
                System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
        } else {
            if (index < nsize) {
                // mHashes数组,index后的hash逐个前移一位
                System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                // mArray数组,(index + 1) << 1后的元素前移
                System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                        (nsize - index) << 1);
            }
            // mArray最后两个空间置空 
            mArray[nsize << 1] = null;
            mArray[(nsize << 1) + 1] = null;
        }
    }
    // 检查是否存在并发修改
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
        throw new ConcurrentModificationException();
    }
    mSize = nsize;
    return (V)old;
}

// 返回ArrayMap保存的键值对数量
@Override
public int size() {
    return mSize;
}

// 检查元素和本实例是否等同
@Override
public boolean equals(Object object) {
    // 为同一个对象返回false
    if (this == object) {
        return true;
    }
    
    // 先检查对象object是否为Map的子类,不是则无法进行比较,返回false
    if (object instanceof Map) {
        Map<?, ?> map = (Map<?, ?>) object;
        // 先检查元素数量,以便快速确认失败并返回false
        if (size() != map.size()) {
            return false;
        }
        
        // 类型一直,数量一样,需要逐个对比元素是否一样
        try {
            for (int i=0; i<mSize; i++) {
                K key = keyAt(i);
                V mine = valueAt(i);
                Object theirs = map.get(key);
                if (mine == null) {
                    if (theirs != null || !map.containsKey(key)) {
                        return false;
                    }
                } else if (!mine.equals(theirs)) {
                    return false;
                }
            }
        } catch (NullPointerException ignored) {
            return false;
        } catch (ClassCastException ignored) {
            // 比较过程中元素类型转换抛出异常,返回false
            return false;
        }
        // 两个map的元素完全相同,返回true
        return true;
    }
    // 两个map的元素不完全相同,返回false
    return false;
}

七、集合操作

private MapCollections<K, V> getCollection() {
    if (mCollections == null) {
        mCollections = new MapCollections<K, V>() {
            @Override
            protected int colGetSize() {
                return mSize;
            }

            @Override
            protected Object colGetEntry(int index, int offset) {
                return mArray[(index<<1) + offset];
            }

            @Override
            protected int colIndexOfKey(Object key) {
                return indexOfKey(key);
            }

            @Override
            protected int colIndexOfValue(Object value) {
                return indexOfValue(value);
            }

            @Override
            protected Map<K, V> colGetMap() {
                return ArrayMap.this;
            }

            @Override
            protected void colPut(K key, V value) {
                put(key, value);
            }

            @Override
            protected V colSetValue(int index, V value) {
                return setValueAt(index, value);
            }

            @Override
            protected void colRemoveAt(int index) {
                removeAt(index);
            }

            @Override
            protected void colClear() {
                clear();
            }
        };
    }
    return mCollections;
}

// 检查ArrayMap是否包含collection中全部元素
public boolean containsAll(Collection<?> collection) {
    return MapCollections.containsAllHelper(this, collection);
}

// 把map变量中的所有键值对存入实例中
@Override
public void putAll(Map<? extends K, ? extends V> map) {
    ensureCapacity(mSize + map.size());
    for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
        put(entry.getKey(), entry.getValue());
    }
}

// 移除ArrayMap中与collection共有的元素
public boolean removeAll(Collection<?> collection) {
    return MapCollections.removeAllHelper(this, collection);
}

// 仅保留ArrayMap中与collection共有的元素
public boolean retainAll(Collection<?> collection) {
    return MapCollections.retainAllHelper(this, collection);
}

// 返回一个包含ArrayMap所有键值对的集合
@Override
public Set<Map.Entry<K, V>> entrySet() {
    return getCollection().getEntrySet();
}

// 返回一个包含ArrayMap所有key的集合
@Override
public Set<K> keySet() {
    return getCollection().getKeySet();
}

// 返回一个包含ArrayMap所有value的集合
@Override
public Collection<V> values() {
    return getCollection().getValues();
}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Google模式

Google模式

Eric Schmidt、Jonathan Rosenberg / 李芳齡 / 天下雜誌出版社 / 2014-11-7 / TWD 420.00

上市即登紐約時報暢銷書、Amazon科技經營排行榜TOP1 未上市即售出美、英、德、日、荷等12國版權 Google創辦人Larry Page專文推薦 第一本由Google領導團隊人首度公開的企業內部運作與思維 Google董事會執行主席艾力克.施密特獨家揭露 Google從崛起到稱霸超過10年的管理與工作秘笈, 以及如何吸引21世紀最搶手的人才-智慧創做者(S......一起来看看 《Google模式》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换