首页 文章 精选 留言 我的

精选列表

搜索[最权威安装],共10000篇文章
优秀的个人博客,低调大师

中国程序员容易发错的单词

作者:三十三重天 最好不是在夕阳西下的时候幻想什么,而是在旭日出生的时候即投入行动!😜 前言 如果你是一位开发工程师,那你一定碰到过这样的情景。 乙方小弟 : "你好,白工。你发的这个摘森和开发文档中的不一致,请核对一下。" 我一惊,什么摘森,我什么时候在文档中有如此神奇的文字描述,莫不是那天正好周五,激动的心颤抖的手将文字打错了。 赶紧看看文档,不然这么低级的错误领导怕不是要Gay死我。 一阵Ctrl+F的文章搜索并没有发现什么摘森的影子。气势汹汹的我立刻一个电话就给干回去了,什么摘森,我什么时候在文档中写了,你看看清楚。 乙方小弟颤栗地说道:”摘森格式啊,我接口调用到的和实际文档中的不一致,我截图给你看。” 我能说什么。。。英文不好不是病,和人讨论真要命 那么问题来了 开发工程量在工作中总会接触到很多的词汇,其中考过框架名称,应用名称,组件名称。我们不能怪起名字的这些大佬为啥要起这么拗口的名字,你行你上啊。咱们只能被动接收这些词汇。 那么问题来了! 如何对一些通用词汇进行正确的口语发音,避免在工作中因为口语不标准,而让别人觉得你很不专业。 隆重推荐 开源项目 中文名称 中国程序员容易发音错误的单词 英文名称 chinese-programmer-wrong-pronunciation 在这里,你可以检索到常用的工程师词汇。针对每个词汇都有对应的美式发音和英式发音音频,这都不是最重要的。 重要的是!!! 有错误发音的音频,你可以动动亲爱的小鼠标,点击错误发音,看看自己是不是中奖,如果有幸中奖,请迅速纠正,然后开始自己的装逼之旅。 截图介绍 当然也有贡献者提供了更骚包的操作 B站Up主针对所有发音进行了整理,并录制了通读视频 👍 基于开源项目上线的网站点击跳转 可视化展现项目内容 👍👍 最后 关注公众号 程序员工具集 👍👍 致力于分享优秀的开源项目、学习资源 、常用工具 回复关键词“关注礼包”,送你一份最全的程序员技能图谱。 回复关键词"wx"添加个人微信,勾搭作者,欢迎来聊^-^。

优秀的个人博客,低调大师

通俗易懂的 HashMap 源码分析解读

文章已经收录在 Github.com/niumoo/JavaNotes ,更有 Java 程序员所需要掌握的核心知识,欢迎Star和指教。 欢迎关注我的公众号,文章每周更新。 HashMap 作为最常用的集合类之一,有必要深入浅出的了解一下。这篇文章会深入到 HashMap 源码,剖析它的存储结构以及工作机制。 1. HashMap 的存储结构 HashMap 的数据存储结构是一个 Node<k,v> 数组,在(Java 7 中是 Entry<k,v> 数组,但结构相同) public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable { // 数组 transient Node<k,v>[] table; static class Node<k,v> implements Map.Entry<k,v> { final int hash; final K key; V value; // 链表 Node<k,v> next; .... } ..... } 存储结构主要是数组加链表,像下面的图。 2. HashMap 的 put() 在 Java 8 中 HashMap 的 put 方法如下,我已经详细注释了重要代码。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // 计算哈希值 与(&amp;)、非(~)、或(|)、异或(^) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<k,v>[] tab; Node<k,v> p; int n, i; // 如果数组为空,进行 resize() 初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果计算的位置上Node不存在,直接创建节点插入 if ((p = tab[i = (n - 1) &amp; hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 如果计算的位置上Node 存在,链表处理 Node<k,v> e; K k; // 如果 hash 值,k 值完全相同,直接覆盖 if (p.hash == hash &amp;&amp;((k = p.key) == key || (key != null &amp;&amp; key.equals(k)))) e = p; // 如果 index 位置元素已经存在,且是红黑树 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) { // 找到节点链表中next为空的节点,创建新的节点插入 p.next = newNode(hash, key, value, null); // 如果节点链表中数量超过TREEIFY_THRESHOLD(8)个,转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 如果节点链表中有发现已有相同key if (e.hash == hash &amp;&amp; ((k = e.key) == key || (key != null &amp;&amp; key.equals(k)))) break; p = e; } } // 如果节点 e 有值,放入数组 table[] 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; } 举个例子,如果 put 的 key 为字母 a,当前 HashMap 容量是初始容量 16,计算出位置是 1。 # int hash = key.hashCode() # hash = hash ^ (hash >>> 16) # 公式 index = (n - 1) &amp; hash // n 是容量 hash HEX(97) = 0110 0001‬ n-1 HEX(15) = 0000 1111 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 总结 HashMap put 过程。 计算 key 的 hash 值。 计算方式是 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 检查当前数组是否为空,为空需要进行初始化,初始化容量是 16 ,负载因子默认 0.75。 计算 key 在数组中的坐标。 计算方式:(容量 - 1) &amp; hash. 因为容量总是2的次方,所以-1的值的二进制总是全1。方便与 hash 值进行与运算。 如果计算出的坐标元素为空,创建节点加入,put 结束。 如果当前数组容量大于负载因子设置的容量,进行扩容。 如果计算出的坐标元素有值。 如果坐标上的元素值和要加入的值 key 完全一样,覆盖原有值。 如果坐标上的元素是红黑树,把要加入的值和 key 加入到红黑树。 如果坐标上的元素和要加入的元素不同(尾插法增加)。 如果 next 节点为空,把要加入的值和 key 加入 next 节点。 如果 next 节点不为空,循环查看 next 节点。 如果发现有 next 节点的 key 和要加入的 key 一样,对应的值替换为新值。 如果循环 next 节点查找超过8层还不为空,把这个位置元素转换为红黑树。 3. HashMap 的 get() 在 Java 8 中 get 方法源码如下,我已经做了注释说明。 public V get(Object key) { Node<k,v> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<k,v> getNode(int hash, Object key) { Node<k,v>[] tab; Node<k,v> first, e; int n; K k; // 只有在存储数组已经存在的情况下进入这个 if if ((tab = table) != null &amp;&amp; (n = tab.length) > 0 &amp;&amp; (first = tab[(n - 1) &amp; hash]) != null) { // first 是获取的坐标上元素 if (first.hash == hash &amp;&amp; // always check first node ((k = first.key) == key || (key != null &amp;&amp; key.equals(k)))) // key 相同,说明first是想要的元素,返回 return first; if ((e = first.next) != null) { if (first instanceof TreeNode) // 如果是红黑树,从红黑树中查找结果 return ((TreeNode<k,v>)first).getTreeNode(hash, key); do { // 循环遍历查找 if (e.hash == hash &amp;&amp; ((k = e.key) == key || (key != null &amp;&amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } get 方法流程总结。 计算 key 的 hash 值。 如果存储数组不为空,且计算得到的位置上的元素不为空。继续,否则,返回 Null。 如果获取到的元素的 key 值相等,说明查找到了,返回元素。 如果获取到的元素的 key 值不相等,查找 next 节点的元素。 如果元素是红黑树,在红黑树中查找。 不是红黑树,遍历 next 节点查找,找到则返回。 4. HashMap 的 Hash 规则 计算 hash 值 int hash = key.hashCode()。 与或上 hash 值无符号右移16 位。 hash = hash ^ (hash >>> 16)。 位置计算公式 index = (n - 1) &amp; hash ,其中 n 是容量。 这里可能会有同学对 hash ^ (hash >>> 16) 有疑惑,很好奇为什么这里要拿 hash 值异或上 hash 值无符号右移 16 位呢?下面通过一个例子演示其中道理所在。 假设 hash 值是 0001 0100 1100 0010 0110 0001‬ 0010 0000,当前容量是 16。 hash = 0001 0100 1100 0010 0110 0001‬ 0010 0000 --- | 与或计算 hash >>> 16 = 0000 0000 0000 0000 0001 0100 1100 0010 --- ------------------------------------------------------ hash 结果 = 0001 0100 1100 0010 0111 0101 1110 0100 --- | &amp; 与运算 容量 -1 = 0000 0000 0000 0000 0000 0000 0000 1111 --- ------------------------------------------------------ # 得到位置 = 0000 0000 0000 0000 0000 0000 0000 0100 得到位置是 4 如果又新增一个数据,得到 hash 值是 0100 0000 1110 0010 1010 0010‬ 0001 0000 ,容量还是16,计算它的位置应该是什么呢? hash = 0100 0000 1110 0010 1010 0010‬ 0001 0000 --- | 与或计算 hash >>> 16 = 0000 0000 0000 0000 0001 0100 1100 0010 --- ------------------------------------------------------ hash 结果 = 0100 0000 1110 0010 1011 0110 1101 0010 --- | &amp; 与运算 容量 -1 = 0000 0000 0000 0000 0000 0000 0000 1111 --- ------------------------------------------------------ # 得到位置 = 0000 0000 0000 0000 0000 0000 0000 0010 得到位置是 2 上面两个例子,得到位置一个是 4,一个是 2,上面只是我随便输入的两个二进制数,那么这两个数如果不经过 hash ^ (hash >>> 16) 运算,位置会有什么变化呢? hash = 0001 0100 1100 0010 0110 0001‬ 0010 0000 容量 -1 = 0000 0000 0000 0000 0000 0000 0000 1111 ------------------------------------------------------ 结果 = 0000 0000 0000 0000 0000 0000 0000 0000 # 得到位置是 0 hash = 0100 0000 1110 0010 1010 0010‬ 0001 0000 容量 -1 = 0000 0000 0000 0000 0000 0000 0000 1111 ------------------------------------------------------ 结果 = 0000 0000 0000 0000 0000 0000 0000 0000 # 得到位置是 0 可以发现位置都是 0 ,冲突概率提高了。可见 hash ^ (hash >>> 16) 让数据的 hash 值的高 16 位与低 16 位进行与或混合,可以减少低位相同时数据插入冲突的概率。 5. HashMap 的初始化大小 初始化大小是 16,为什么是 16 呢? 这可能是因为每次扩容都是 2 倍。而选择 2 的次方值 16 作为初始容量,有利于扩容时重新 Hash 计算位置。为什么是 16 我想是一个经验值,理论上说只要是 2 的次方都没有问题。 6. HashMap 的扩容方式 负载因子是多少?负载因子是 0.75。 扩容方式是什么?看源码说明。 /** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<k,v>[] resize() { Node<k,v>[] oldTab = table; // 现有容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 现有扩容阀值 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 如果当前长度已经大于最大容量。结束扩容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 如果扩大两倍之后小于最大容量,且现有容量大于等于初始容量,就扩大两倍 else if ((newCap = oldCap &lt;&lt; 1) &lt; MAXIMUM_CAPACITY &amp;&amp; oldCap >= DEFAULT_INITIAL_CAPACITY) // 扩容阀值扩大为两倍 newThr = oldThr &lt;&lt; 1; // double threshold } // 当前容量 = 0 ,但是当前记录容量 > 0 ,获取当前记录容量。 else if (oldThr > 0) // initial capacity was placed in threshold // 进入这里,说明是通过指定容量和负载因子的构造函数 newCap = oldThr; else { // zero initial threshold signifies using defaults // 进入这里说明是通过无参构造 // 新的容量 newCap = DEFAULT_INITIAL_CAPACITY; // 新的阀值 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; // 计算扩容阀值 newThr = (newCap &lt; MAXIMUM_CAPACITY &amp;&amp; ft &lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<k,v>[] newTab = (Node<k,v>[])new Node[newCap]; table = newTab; // 如果 oldTab != null,说明是扩容,否则是初始化,直接返回 if (oldTab != null) { for (int j = 0; j &lt; oldCap; ++j) { Node<k,v> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果当前元素 next节点没有元素,当前元素重新计算位置直接放入 if (e.next == null) newTab[e.hash &amp; (newCap - 1)] = e; else if (e instanceof TreeNode) // 如果当前节点是红黑树 ((TreeNode<k,v>)e).split(this, newTab, j, oldCap); else { // preserve order Node<k,v> loHead = null, loTail = null; Node<k,v> hiHead = null, hiTail = null; Node<k,v> next; do { next = e.next; // == 0 ,位置不变 if ((e.hash &amp; oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // e.hash &amp; oldCap != 0 ,位置变为:位置+扩容前容量 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 扩容时候怎么重新确定元素在数组中的位置,我们看到是由 if ((e.hash &amp; oldCap) == 0) 确定的。 hash HEX(97) = 0110 0001‬ n HEX(16) = 0001 0000 -------------------------- 结果 = 0000 0000 # e.hash &amp; oldCap = 0 计算得到位置还是扩容前位置 hash HEX(17) = 0001 0001‬ n HEX(16) = 0001 0000 -------------------------- 结果 = 0001 0000 # e.hash &amp; oldCap != 0 计算得到位置是扩容前位置+扩容前容量 通过上面的分析也可以看出,只有在每次容量都是2的次方的情况下才能使用 if ((e.hash &amp; oldCap) == 0) 判断扩容后的位置。 7. HashMap 中的红黑树 HashMap 在 Java 8 中的实现增加了红黑树,当链表节点达到 8 个的时候,会把链表转换成红黑树,低于 6 个的时候,会退回链表。究其原因是因为当节点过多时,使用红黑树可以更高效的查找到节点。毕竟红黑树是一种二叉查找树。 节点个数是多少的时候,链表会转变成红黑树。 链表节点个数大于等于 8 时,链表会转换成树结构。 节点个数是多少的时候,红黑树会退回链表。 节点个数小于等于 6 时,树会转变成链表。 为什么转变条件 8 和 6 有一个差值。 如果没有差值,都是 8 ,那么如果频繁的插入删除元素,链表个数又刚好在 8 徘徊,那么就会频繁的发生链表转树,树转链表。 8. 为啥容量都是2的幂? 容量是2的幂时,key 的 hash 值然后 &amp; (容量-1) 确定位置时碰撞概率会比较低,因为容量为 2 的幂时,减 1 之后的二进制数为全1,这样与运算的结果就等于 hash值后面与 1 进行与运算的几位。 下面是个例子。 hash HEX(97) = 0110 0001‬ n-1 HEX(15) = 0000 1111 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 hash HEX(99) = 0110 0011‬ n-1 HEX(15) = 0000 1111 -------------------------- 结果 = 0000 0011 # 计算得到位置是 3 hash HEX(101) = 0110 0101‬ n-1 HEX(15) = 0000 1111 -------------------------- 结果 = 0000 0101 # 计算得到位置是 5 如果是其他的容量值,假设是9,进行与运算结果碰撞的概率就比较大。 hash HEX(97) = 0110 0001‬ n-1 HEX(09) = 0000 1001 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 hash HEX(99) = 0110 0011‬ n-1 HEX(09) = 0000 1001 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 hash HEX(101) = 0110 0101‬ n-1 HEX(09) = 0000 1001 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 另外,每次都是 2 的幂也可以让 HashMap 扩容时可以方便的重新计算位置。 hash HEX(97) = 0110 0001‬ n-1 HEX(15) = 0000 1111 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 hash HEX(97) = 0110 0001‬ n-1 HEX(31) = 0001 1111 -------------------------- 结果 = 0000 0001 # 计算得到位置是 1 9. 快速失败(fail—fast) HashMap 遍历使用的是一种快速失败机制,它是 Java 非安全集合中的一种普遍机制,这种机制可以让集合在遍历时,如果有线程对集合进行了修改、删除、增加操作,会触发并发修改异常。 它的实现机制是在遍历前保存一份 modCount ,在每次获取下一个要遍历的元素时会对比当前的 modCount 和保存的 modCount 是否相等。 快速失败也可以看作是一种安全机制,这样在多线程操作不安全的集合时,由于快速失败的机制,会抛出异常。 10. 线程安全的 Map 使用 Collections.synchronizedMap(Map) 创建线程安全的 Map. 实现原理:有一个变量 final Object mutex; ,操作方法都加了这个 synchronized (mutex) 排它锁。 使用 Hashtable 使用 ConcurrentHashMap 最后的话 文章已经收录在 Github.com/niumoo/JavaNotes ,欢迎Star和指教。更有一线大厂面试点,Java程序员需要掌握的核心知识等文章,也整理了很多我的文字,欢迎 Star 和完善,希望我们一起变得优秀。 文章有帮助可以点个「赞」或「分享」,都是支持,我都喜欢! 文章每周持续更新,要实时关注我更新的文章以及分享的干货,可以关注「 未读代码 」公众号或者我的博客。

优秀的个人博客,低调大师

威胁情报:危险的网络安全工作

威胁情报、APT分析师、事件检测和响应专家、欺骗式或攻击性防御技术开发者和渗透测试人员们经常需要出没暗网、分析危险的恶意软件,或者追踪危险的网络犯罪分子。他们是企业网络安全的“奇兵”和特勤部队,有时也是P4级别的“病毒实验室”的操作者,此类职业的危险性不仅仅是个人信息泄露、违规或者“顶锅”、“问责”等岗位风险,更大的风险在于他们中很多人都在缺乏监督和培训的“无防护”状态下工作,随时有可能成为企业自身网络安全和公共安全的“殉爆”炸弹(尤其是威胁情报工作)。 糟糕的是,超过三分之一的威胁情报人员居然没有任何OSINT经验: 更加糟糕的是,如此菜鸟云集的高危岗位,却严重缺乏必要的培训和安全管控措施。 《报告》发现威胁情报人员普遍缺乏必要的安全培训、审计和监控。85%的网络威胁情报(CTI)专业人员很少或根本没有接受过对于确保公司和公共安全至关重要的在线活动的培训。这导致职业风险的急剧上升: 38%的CTI人员不使用托管归因工具掩盖或隐藏其在线身份或角色; 29%的CTI人员汇报缺乏监督程序,以确保分析人员不会滥用工具; 54%的CTI人员缺乏安全指导规范。 《报告》还指出,导致威胁情报工作更加危险的原因主要有两点: 两大原因 缺乏培训,你很难想象让一群未经培训的员工去操纵危险武器而不出意外。 缺乏审计和监控,将近30%的企业未能对CTI员工违规或滥用资源实施有效监控。 留神迭代中的法律雷区 随着各国网络安全法案的不断完善,威胁情报工作者还需要警惕不断累积的法律风险。 威胁情报发布方面,企业需要留意2019年11月20日国家互联网信息办公室发布的《网络安全威胁信息发布管理办法(征求意见稿)》,《办法》首次对威胁情报发布做出明确规定,例如个人或企业发布网络安全威胁信息时标题中不得含有“预警“字样,同时《办法》还对威胁情报发布前的汇报实体和发布形式等给出了规范。对于广大网络安全企业来说,尤其要留意《办法》中的这段话: 部分网络安全企业和机构为推销产品、赚取眼球,不当评价有关地区、行业网络安全攻击、事件、风险、脆弱性状况,误导舆论,造成不良影响;部分媒体、网络安全企业随意发布网络安全预警信息,夸大危害和影响,容易造成社会恐慌。 在威胁情报采集和追踪方面,企业需要留意今年2月份美国司法部发布的《网络威胁情报采集与数据购买法律指南1.0版》,首次对威胁情报采集和黑市交易给出出了明确的法律建议。 根据《指南》,在暗网收集情报或者购买数据方面的小差错,最终可能导致威胁情报工作者陷入严重的法律麻烦中。威胁情报公司Recorded Future指出: 弄错这些规则的风险很大。根据相关联邦法规,个人不仅会被处以高额的刑事罚款,而且可能会被判处长达20年的监禁。 安全牛在阅读《指南》后发现,该指南给出了两个基本暗网情报行为准则:1.不要犯事。2.不要成为受害者。所谓的不要犯事,主要是指不要主动与论坛中的成员沟通交易,潜水观察,被动采集信息的法律风险很小。另外两个被明确提出的雷区是: 不要使用失窃账号(可以继续使用伪造账号)。 与犯罪分子谈判以检索或索要被盗数据(例如勒索软件或者数据泄露缓解)的组织也需要格外小心。从不法分子手中购买自己的数据似乎没有法律风险,但是,如果卖方不小心将其他被盗数据包括在其中,尤其是被盗的知识产权、信用卡号等数据,则可能惹上法律风险。此外,如果犯罪实体正好被贴上了恐怖组织的标签,或被归类于出口管制法规,则任何与之进行谈判(哪怕目的是取回自己的数据)的组织都可能因此而接受有关部门调查。 对于企业安全主管来说,鉴于威胁情报采集活动的国际化属性,应当根据我国和其他国家相关网络安全法规,制定清晰明确的威胁情报参与规则,阐明法律责任和协议,明确在进行威胁情报收集时什么可以做,什么不可以做。在网络安全合规全球化的今天,跨国公司或者拥有海外业务的企业开展威胁情报工作可能面临(跨国)民事、刑事或监管的情况下,不断修订完善的明文规则对于降低威胁情报活动的风险将会很有用。 【本文是51CTO专栏作者“安全牛”的原创文章,转载请通过安全牛(微信公众号id:gooann-sectv)获取授权】 戳这里,看该作者更多好文

优秀的个人博客,低调大师

全网通俗易懂的【短链接】入门

前言 只有光头才能变强。 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 最近接了一个需求,涉及到了短链接的相关的知识,于是去查阅了相关的资料,在这里给大家整理分享一下。 一、短链接介绍 举个例子,现在我的GitHub的地址是这个:https://github.com/ZhongFuCheng3y/3y (36个字符) 我通过百度的短链接服务可以将上面的地址转成https://dwz.cn/LwlrfG4j(23个字符) 转短链接 那我为什么要将原有的URL转成较短的链接呢?比如我们发短信提醒用户去XXX,XXX有优惠活动,在文案上往往会带有一个链接进行跳转,方便用户快速去到对应的活动落地页。 而短信的发送是需要成本的,短信的成本主要有两方面组成: 发送的人数(发的人越多,自然短信的花费就越大,这个我就不解释了) 短信发送的字数(比如,文案总字数超过70个字,那就算两条短信计费,超过140个字就算三条短信计费) 所以在发送短信给用户时:要么就投放更加精准优质的用户,以便控制好发送的数量,要么就尽可能控制文案的字数。 显然,如果在短信上配上普通的URL,那真正的文案可写的字数就没多少了。于是我们可以发现,各大公司的短信推送的URL都是短链接。 短链接案例之一 比如在一些平台发布消息时会限制字数,如果我们的发的URL过长就很容易就被限制住了: 限制字数 使用短链接的好处:短、字符少、美观、便于发布、传播。 二、短链接它是怎么干的呢? 我们先回到生成好的短链上https://dwz.cn/LwlrfG4j 虽然这个链接看起来有点奇怪,但他终究还是一个链接,从URL的特征我们可以分出: dwz.cn是域名 LwlrfG4j是参数 域名 我们在浏览器请求一下短链接看看是什么情况: 302跳转 短链接的原理其实就是: 将长链接通过一定的手段生成一个短链接 访问短链接时实际访问的是短链接服务器,然后根据短链接的参数找回对应的长链接 重定向跳转 大致原理图 2.1 核心的要解决的问题 通过上面的分析我们可以知道的是,我们实际核心要做的是怎么从LwlrfG4j类似这样的参数找到对应的完整URL:https://github.com/ZhongFuCheng3y/3y 脑子第一时间想到的是:能不能通过一个压缩算法将https://github.com/ZhongFuCheng3y/3y压缩更小的字符? 显然,不能,压缩算法大多数都是针对大文本才奏效,本身的URL也不见得有多大…压缩出来肯定比原来的URL还大。 脑子第二时间想到的是:能不能用Hash算法?还是不能,用Hash存在哈希碰撞的问题 什么是哈希碰撞?两个不相同的字符串(值)进行Hash操作后,得到的哈希值相同。 这就意味着,两个完全不同的长链得到的哈希值一模一样,而我的短链是依赖哈希值去找到长链的(此时一个短链对应多个长链,这不合理)。 脑子第三时间想到的是?脑子想不到了。 现在业内用得比较多的是发号器(ID自增)+62进制编码: 比如,我将https://github.com/ZhongFuCheng3y/3y看作是10000,然后将10000进行62进制编码得到的结果是:2Bi 那我的短链URL就可以弄成https://3y.cn/2Bi,其中3y.cn是域名,2Bi是经过62进制转换后的参数。 为什么要用62进制转换?64进制转换倒是听得多了 62进制转换是因为62进制转换后只含数字+小写+大写字母。而64进制转换会含有/,+这样的符号(不符合正常URL的字符) 10进制转62进制可以缩短字符,如果我们要6位字符的话,已经有560亿个组合了。 6位字符 总结: ID自增后,转成62进制,在DB保存映射关系,生成短链接 短链接过程 三、短信的链接直接跳转到APP 以下内容来源:https://sq.163yun.com/blog/article/158315832059072512 ,作者:西西吹雪 综合起来就是: 通过 Deep Links(iOS 则是Universal Links),可以实现点击短信链接直接唤起 App; 如果系统因为各种原因不支持 Deep Links,备选方案是 intent filter,不过会出弹框让用户选择用哪个 App 打开链接; 如果用户没有选择我们的 App 而是选择了浏览器打开,则通过 自定义 scheme 尝试唤起 App; 由于技术和成本问题,我们忽略不支持 自定义 scheme 的浏览器。 短信链接唤醒APP 最后 这篇文章主要是简单了解一下短链接的相关知识,一个完备的短链服务肯定还要考虑更多的事,这里我就不展开了(毕竟我也没真正写过,可以在下方的链接继续学习)~ 更多资料查阅: https://www.zhihu.com/question/29270034/answer/46446911 https://hufangyun.com/2017/short-url/ https://blog.csdn.net/c10WTiybQ1Ye3/article/details/78098840 本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y 乐于输出干货的Java技术公众号:Java3y。公众号内有300多篇原创技术文章、海量视频资源、精美脑图,关注即可获取! 转发到朋友圈是对我最大的支持! 非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「三歪」我有点东西的话 求点赞 求关注️ 求分享👥 求留言💬 对暖男我来说真的 非常有用!!! 创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

优秀的个人博客,低调大师

Java 搞定 SQL 集合运算的简方法

问题介绍 作为 java 程序员,用代码直接实现类似 SQL 中的交并补差的集合运算,总是要编写大量的代码,如果能有一个专门的外部数据工具,通过写简单类似 SQL 的脚本来实现,在 java 中直接调用并可以返回结果集,就再好不过了。Java 版集算器正是解决这一难题的神器,通过 SPL 脚本可以直观自然得写出运算,再使用 java 调用 SPL 脚本,使用起来简单,快捷,高效。另外,虽然 SQL 有集合概念,但对于有序集合运算提供的支持却很有限,经常要采用很费解的思路才可以完成, SPL 基于离散数据集模型,能轻松处理有序集合运算。下面我们就由浅入深,举例说明如何使用。 SPL 实现和集 示例 1: 求重叠时间段的总天数 MySQL8: with recursive t(start,end) as (select date'2010-01-07',date'2010-01-9' union all select date'2010-01-15',date'2010-01-16' union all select date'2010-01-07',date'2010-01-12' union all select date'2010-01-08',date'2010-01-11'), t1(d,end) as (select start,end from t union all select d+1,end from t1 where d select count(distinct d) from t1; 说明:此例先将各时间段转成时间段内所有日子对应的日期,然后再求不同日期的个数 集算器 SPL: A3: 对 A2 中的每一个时间段构造从 start 到 end 的日期序列 A4: 求 A3 中所有日期序列的和 A5: 求 A4 中不重复日期的个数保存脚本文件SumSet.dfx (嵌入 Java 会用到) 差集 示例 1: 列出英语人口和法语人口均超过 5% 的国家 MySQL8: with t1(lang) as (select 'English' union all select 'French') select name from world.country c where not exists(select * from t1 where lang not in (select language from world.countrylanguage where percentage>=5 and countrycode=c.code ) ); 说明:此 SQL 只是演示通过双重否定实现差集为空 集算器 SPL:A4: 选出 [“English”,”French”] 与本组语言集合的差为空的组,意思就是选出语言集合包含 English 和 French 的组保存脚本文件DifferenceSet.dfx (嵌入 Java 会用到) 交集 示例 1: 列出英语人口、法语人口、西班牙语人口分别超过 0.3%、0.2%、0.1% 的国家代码 MySQL8: with t1 as (select countrycode from world.countrylanguage where language='English' and percentage>0.3), t2 as (select countrycode from world.countrylanguage where language='French' and percentage>0.2), t3 as (select countrycode from world.countrylanguage where language='Spanish' and percentage>0.1) select countrycodefrom t1 join t2 using(countrycode) join t3 using(countrycode); 说明:此例只是演示如何求解多个集合的交集 集算器 SPL:A3: 按次序依次查询英语人口超 0.3%、法语人口超 0.2%、西班牙语超 0.1% 的国家代码,并转成序列 A5: A3 中所有序列交集 保存脚本文件IntersectionSet.dfx (嵌入 Java 会用到) Java 调用 SPL 嵌入到 Java 应用程序十分方便,通过 JDBC 调用存储过程方式加载,用和集保存的文件SumSet.dfx,示例调用如下: ... Connection con = null; Class.forName("com.esproc.jdbc.InternalDriver"); con= DriverManager.getConnection("jdbc:esproc:local://"); //调用存储过程,其中SumSet是dfx的文件名 st =(com. esproc.jdbc.InternalCStatement)con.prepareCall("call SumSet()"); //执行存储过程 st.execute(); //获取结果集 ResultSet rs = st.getResultSet(); ... 替换成DifferenceSet.dfx或IntersectionSet.dfx是同样的道理,只需 call DifferenceSet()或者 call IntersectionSet() 即可。这里只用 Java 片段粗略解释了如何嵌入 SPL,详细步骤请参阅Java 如何调用 SPL 脚本​,也非常简单,不再赘述。同时,SPL 也支持 ODBC 驱动,集成到支持 ODBC 的语言,嵌入过程类似。 扩展节选 关于集合运算除了上面讲的和差交运算,还可以获取与行号有关的计算,以及有序集合的对位运算。 根据行号取数据 示例 1: 计算招商银行 (600036) 2017 年第 3 个交易日和倒数第 3 个交易日的交易信息 MySQL8: with t as (select *, row_number() over(order by tdate) rn from stktrade where sid='600036' and tdate between '2017-01-01' and '2017-12-31') select tdate,open,close,volume from t where rn=3 union all select tdate,open,close,volume from t where rn=(select max(rn)-2 from t); 集算器 SPL: 示例 2: 计算招商银行 (600036) 最近 20 个交易日的平均收盘价 MySQL8: with t as (select *, row_number() over(order by tdate desc) rn from stktrade where sid='600036')select avg(close) avg20 from t where rn<=20;集算器 SPL:求满足条件的记录的行号 示例 1: 计算招商银行 (600036)2017 年经过多少交易日收盘价达到 25 元 MySQL8: with t as (select *, row_number() over(order by tdate) rn from stktrade where sid='600036' and tdate between '2017-01-01' and '2017-12-31')select min(rn) from t where close>=25;集算器 SPL: 示例 2: 计算格力电器 (000651) 2017 年涨幅 (考虑停牌) MySQL8: with t as (select * from stktrade where sid='000651'), t1(d) as (select max(tdate) from t where tdate<'2017-01-01'), t2(d) as (select max(tdate) from t where tdate<'2018-01-01') select s2.close/s1.close-1 risefrom (select * from t,t1 where tdate=d) s1, (select * from t,t2 where tdate=d) s2; 集算器 SPL: A2: 数据按交易日从小到大排序 A3: 从后往前查找交易日在 2017-01-01 之前的最后一条记录在序列中的行号 A4: 求 2016 年收盘价 A5: 求 2017 年收盘价,其中 A2.m(-1) 取倒数第 1 条记录,即 2017 年最后一个交易日对应的记录 示例 3: 列出 2017 年信息发展 (300469) 交易量超过 250 万股时的交易信息及各日涨幅(考虑停牌) MySQL8: with t as (select *, row_number() over(order by tdate) rn from stktrade where sid='300469' and tdate<=date '2017-12-31'), t1 as (select * from t where tdate>=date'2017-01-01' and volume>=2500000) select t1.tdate, t1.close, t.volume, t1.close/t.close-1 rise from t1 join t on t1.rn=t.rn+1; 集算器 SPL:求最大值或最小值所在记录的行号 示例 1: 计算招商银行 (600036) 2017 年最早的最低价与最早的最高价间隔多少交易日 MySQL8: with t as (select *, row_number() over(order by tdate) rn from stktrade where sid='600036' and tdate between '2017-01-01' and '2017-12-31'), t1 as (select * from t where close=(select min(close) from t)), t2 as (select * from t where close=(select max(close) from t)) select abs(cast(min(t1.rn) as signed)-cast(min(t2.rn) as signed)) intevalfrom t1,t2;集算器 SPL:示例 2: 计算招商银行 (600036) 2017 年最后的最低价与最后的最高价间隔多少交易日 MySQL8: with t as (select *, row_number() over(order by tdate) rn from stktrade where sid='600036' and tdate between '2017-01-01' and '2017-12-31'), t1 as (select * from t where close=(select min(close) from t)), t2 as (select * from t where close=(select max(close) from t)) select abs(cast(max(t1.rn) as signed)-cast(max(t2.rn) as signed)) intevalfrom t1,t2;集算器 SPL:有序集合间的对位计算 示例 1: 求 2018 年 3 月 6 日到 8 日创业板指 (399006) 对深证成指 (399001) 的每日相对收益率 MySQL8: with t1 as (select *,close/lag(close) over(order by tdate) rise from stktrade where sid='399006' and tdate between '2018-03-05' and '2018-03-08'), t2 as (select *, close/lag(close) over(order by tdate) rise from stktrade where sid='399001' and tdate between '2018-03-05' and '2018-03-08') select t1.rise-t2.risefrom t1 join t2 using(tdate)where t1.rise is not null;集算器 SPL:SPL 优势有库写 SQL,没库写 SPL 用 Java 程序直接汇总计算数据,还是比较累的,代码很长,并且不可复用,很多情况数据也不在数据库里,有了 SPL,就能像在 Java 中用 SQL 一样了,十分方便。 常用无忧,不花钱就能取得终身使用权的入门版 如果要分析的数据是一次性或临时性的,润乾集算器每个月都提供免费试用授权,可以循环免费使用。但要和 Java 应用程序集成起来部署到服务器上长期使用,定期更换试用授权还是比较麻烦,润乾提供了有终身使用权的入门版,解决了这个后顾之忧,获得方式参考 如何免费使用润乾集算器? 技术文档和社区支持 官方提供的集算器技术文档本身就有很多现成的例子,常规问题从文档里都能找到解决方法。如果获得了入门版,不仅能够使用 SPL 的常规功能,碰到任何问题都可以去乾学院上去咨询,官方通过该社区对入门版用户提供免费的技术支持。

优秀的个人博客,低调大师

2019五个棒的机器学习课程

凭借强大的统计学基础,机器学习正在成为最有趣,节奏最快的计算机科学领域之一,目前已经有无穷无尽的行业和应用正在使用机器学习使它们更高效和智能。 聊天机器人、垃圾邮件过滤、广告投放、搜索引擎和欺诈检测是机器学习模型正在实际应用于日常生活的几个例子。 机器学习到底是什么呢?我认为机器学习是让我们找到模式并为人类无法做的事情创建数学模型。 机器学习课程与包含探索性数据分析,统计,通信和可视化技术等主题的数据科学课程不同,它更侧重于教授机器学习算法,如何以数学方式工作,以及如何在编程语言中使用它们。 以下是今年五大机器学习课程的简要回顾。 最好的五个机器学习课程: 1.机器学习-Coursera 2.深度学习专项课程-Coursera 3.使用Python进行机器学习-Coursera 4.高级机器学习专项课程-Coursera 5.机器学习-Ed

优秀的个人博客,低调大师

Android 简单的限制输入方式之一

今天带来工作中的一个小安利,产品要求对用户名输入需要限制,只能是数字和字母,符号,不能包含空格和键盘上输入的emoji.开始拿到这个需求,觉得给 EditText 增加一个 addTextChangedListener ,里面做各种判断不就OK 啦! 哈哈,又可以愉快的玩耍咯… 但是回调里面逻辑太多,看着也不爽,不符合我们程序员的气质,简洁大方,干净利落!所以我特意去看了 du 了一下, 结合自己的实际要求,重写了 EditText 的 onCreateInputConnection() 方法,在那里做文章,请看下面源码(如果还有不清楚的,可以留言或者看Github地址) 只需要自定义EditText重写其onCreateInputConnection()方法,然后再定义一个内部类就好,下面代码即拷即用 首先,看看 LimitEditText classLimitEditText(context:Context,attrs:AttributeSet,defStyleAttr:Int) :EditText(context,attrs,defStyleAttr){ constructor(context:Context,attrs:AttributeSet):this(context,attrs,0) /** *输入法 */ overridefunonCreateInputConnection(outAttrs:EditorInfo?):InputConnection{ returnInnerInputConnection(super.onCreateInputConnection(outAttrs),false) } }classInnerInputConnection(target:InputConnection,mutable:Boolean) :InputConnectionWrapper(target,mutable){ //数字,字母 privatevalpattern=Pattern.compile("^[0-9A-Za-z_]\$") //标点 privatevalpatternChar=Pattern.compile("[^\\w\\s]+") //EmoJi privatevalpatternEmoJi=Pattern.compile("[\ud83c\udc00-\ud83c\udfff]|[\ud83d\udc00-\ud83d\udfff]|[\u2600-\u27ff]",Pattern.UNICODE_CASEorPattern.CASE_INSENSITIVE) //英文标点 privatevalpatternEn=Pattern.compile("^[`~!@#\$%^&*()_\\-+=<>?:\"{},.\\\\/;'\\[\\]]\$") //中文标点 privatevalpatternCn=Pattern.compile("^[·!#¥(——):;“”‘、,|《。》?、【】\\[\\]]\$") //对输入拦截 overridefuncommitText(text:CharSequence?,newCursorPosition:Int):Boolean{ if(patternEmoJi.matcher(text).find()){ returnfalse } if(pattern.matcher(text).matches()||patternChar.matcher(text).matches()){ returnsuper.commitText(text,newCursorPosition) } returnfalse } } 总计60行代码,可以搞定一般需求啦,再来看看其布局用法(xml文件),平时怎么在布局写EditText,还是怎么写! <?xmlversion="1.0"encoding="utf-8"?><android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="cn.molue.jooyer.limitedittext.MainActivity"> <cn.molue.jooyer.limitedittext.LimitEditText android:id="@+id/let_main" android:layout_width="match_parent" android:layout_height="50dp" android:layout_margin="10dp" android:text="HelloWorld!" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/></android.support.constraint.ConstraintLayout> 最后来看看在 Activity 中用法,其实和一般普通 EditText 用法一致啦! classMainActivity:AppCompatActivity(){ overridefunonCreate(savedInstanceState:Bundle?){ super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //demo中默认LimitEditText只能输入字母数字和标点符号 //延时主要是更方便观察 window.decorView.postDelayed({ //注意,获得焦点需要自己再处理下,其实很简单,如下: let_main.isFocusable=true let_main.isFocusableInTouchMode=true let_main.requestFocus() },1000) } } 当然,这些限制正则也可以在 LimitEditText 中定义方法,大家需要什么加入什么就好了! 原文发布时间为:2018-11-20 本文来自云栖社区合作伙伴“Android开发中文站”,了解相关信息可以关注“Android开发中文站”。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。