add some source code
This commit is contained in:
parent
b823b8e43e
commit
2201be708e
2984
notes/src/String.java
Normal file
2984
notes/src/String.java
Normal file
File diff suppressed because it is too large
Load Diff
527
notes/src/StringBuffer.java
Normal file
527
notes/src/StringBuffer.java
Normal file
@ -0,0 +1,527 @@
|
||||
|
||||
|
||||
package java.lang;
|
||||
|
||||
public final class StringBuffer
|
||||
extends AbstractStringBuilder
|
||||
implements java.io.Serializable, CharSequence
|
||||
{
|
||||
|
||||
/** use serialVersionUID from JDK 1.0.2 for interoperability */
|
||||
static final long serialVersionUID = 3388685877147921107L;
|
||||
|
||||
/**
|
||||
* Constructs a string buffer with no characters in it and an
|
||||
* initial capacity of 16 characters.
|
||||
*/
|
||||
public StringBuffer() {
|
||||
super(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string buffer with no characters in it and
|
||||
* the specified initial capacity.
|
||||
*
|
||||
* @param capacity the initial capacity.
|
||||
* @exception NegativeArraySizeException if the <code>capacity</code>
|
||||
* argument is less than <code>0</code>.
|
||||
*/
|
||||
public StringBuffer(int capacity) {
|
||||
super(capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string buffer initialized to the contents of the
|
||||
* specified string. The initial capacity of the string buffer is
|
||||
* <code>16</code> plus the length of the string argument.
|
||||
*
|
||||
* @param str the initial contents of the buffer.
|
||||
* @exception NullPointerException if <code>str</code> is <code>null</code>
|
||||
*/
|
||||
public StringBuffer(String str) {
|
||||
super(str.length() + 16);
|
||||
append(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string buffer that contains the same characters
|
||||
* as the specified <code>CharSequence</code>. The initial capacity of
|
||||
* the string buffer is <code>16</code> plus the length of the
|
||||
* <code>CharSequence</code> argument.
|
||||
* <p>
|
||||
* If the length of the specified <code>CharSequence</code> is
|
||||
* less than or equal to zero, then an empty buffer of capacity
|
||||
* <code>16</code> is returned.
|
||||
*
|
||||
* @param seq the sequence to copy.
|
||||
* @exception NullPointerException if <code>seq</code> is <code>null</code>
|
||||
* @since 1.5
|
||||
*/
|
||||
public StringBuffer(CharSequence seq) {
|
||||
this(seq.length() + 16);
|
||||
append(seq);
|
||||
}
|
||||
|
||||
public synchronized int length() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized int capacity() {
|
||||
return value.length;
|
||||
}
|
||||
|
||||
|
||||
public synchronized void ensureCapacity(int minimumCapacity) {
|
||||
if (minimumCapacity > value.length) {
|
||||
expandCapacity(minimumCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized void trimToSize() {
|
||||
super.trimToSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @see #length()
|
||||
*/
|
||||
public synchronized void setLength(int newLength) {
|
||||
super.setLength(newLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @see #length()
|
||||
*/
|
||||
public synchronized char charAt(int index) {
|
||||
if ((index < 0) || (index >= count))
|
||||
throw new StringIndexOutOfBoundsException(index);
|
||||
return value[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized int codePointAt(int index) {
|
||||
return super.codePointAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized int codePointBefore(int index) {
|
||||
return super.codePointBefore(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized int codePointCount(int beginIndex, int endIndex) {
|
||||
return super.codePointCount(beginIndex, endIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized int offsetByCodePoints(int index, int codePointOffset) {
|
||||
return super.offsetByCodePoints(index, codePointOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized void getChars(int srcBegin, int srcEnd, char[] dst,
|
||||
int dstBegin)
|
||||
{
|
||||
super.getChars(srcBegin, srcEnd, dst, dstBegin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @see #length()
|
||||
*/
|
||||
public synchronized void setCharAt(int index, char ch) {
|
||||
if ((index < 0) || (index >= count))
|
||||
throw new StringIndexOutOfBoundsException(index);
|
||||
value[index] = ch;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(Object obj) {
|
||||
super.append(String.valueOf(obj));
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(String str) {
|
||||
super.append(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the specified <tt>StringBuffer</tt> to this sequence.
|
||||
* <p>
|
||||
* The characters of the <tt>StringBuffer</tt> argument are appended,
|
||||
* in order, to the contents of this <tt>StringBuffer</tt>, increasing the
|
||||
* length of this <tt>StringBuffer</tt> by the length of the argument.
|
||||
* If <tt>sb</tt> is <tt>null</tt>, then the four characters
|
||||
* <tt>"null"</tt> are appended to this <tt>StringBuffer</tt>.
|
||||
* <p>
|
||||
* Let <i>n</i> be the length of the old character sequence, the one
|
||||
* contained in the <tt>StringBuffer</tt> just prior to execution of the
|
||||
* <tt>append</tt> method. Then the character at index <i>k</i> in
|
||||
* the new character sequence is equal to the character at index <i>k</i>
|
||||
* in the old character sequence, if <i>k</i> is less than <i>n</i>;
|
||||
* otherwise, it is equal to the character at index <i>k-n</i> in the
|
||||
* argument <code>sb</code>.
|
||||
* <p>
|
||||
* This method synchronizes on <code>this</code> (the destination)
|
||||
* object but does not synchronize on the source (<code>sb</code>).
|
||||
*
|
||||
* @param sb the <tt>StringBuffer</tt> to append.
|
||||
* @return a reference to this object.
|
||||
* @since 1.4
|
||||
*/
|
||||
public synchronized StringBuffer append(StringBuffer sb) {
|
||||
super.append(sb);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends the specified <code>CharSequence</code> to this
|
||||
* sequence.
|
||||
* <p>
|
||||
* The characters of the <code>CharSequence</code> argument are appended,
|
||||
* in order, increasing the length of this sequence by the length of the
|
||||
* argument.
|
||||
*
|
||||
* <p>The result of this method is exactly the same as if it were an
|
||||
* invocation of this.append(s, 0, s.length());
|
||||
*
|
||||
* <p>This method synchronizes on this (the destination)
|
||||
* object but does not synchronize on the source (<code>s</code>).
|
||||
*
|
||||
* <p>If <code>s</code> is <code>null</code>, then the four characters
|
||||
* <code>"null"</code> are appended.
|
||||
*
|
||||
* @param s the <code>CharSequence</code> to append.
|
||||
* @return a reference to this object.
|
||||
* @since 1.5
|
||||
*/
|
||||
public StringBuffer append(CharSequence s) {
|
||||
// Note, synchronization achieved via other invocations
|
||||
if (s == null)
|
||||
s = "null";
|
||||
if (s instanceof String)
|
||||
return this.append((String)s);
|
||||
if (s instanceof StringBuffer)
|
||||
return this.append((StringBuffer)s);
|
||||
return this.append(s, 0, s.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized StringBuffer append(CharSequence s, int start, int end)
|
||||
{
|
||||
super.append(s, start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(char[] str) {
|
||||
super.append(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized StringBuffer append(char[] str, int offset, int len) {
|
||||
super.append(str, offset, len);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(boolean b) {
|
||||
super.append(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(char c) {
|
||||
super.append(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(int i) {
|
||||
super.append(i);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized StringBuffer appendCodePoint(int codePoint) {
|
||||
super.appendCodePoint(codePoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(long lng) {
|
||||
super.append(lng);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(float f) {
|
||||
super.append(f);
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized StringBuffer append(double d) {
|
||||
super.append(d);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized StringBuffer delete(int start, int end) {
|
||||
super.delete(start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized StringBuffer deleteCharAt(int index) {
|
||||
super.deleteCharAt(index);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized StringBuffer replace(int start, int end, String str) {
|
||||
super.replace(start, end, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized String substring(int start) {
|
||||
return substring(start, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.4
|
||||
*/
|
||||
public synchronized CharSequence subSequence(int start, int end) {
|
||||
return super.substring(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized String substring(int start, int end) {
|
||||
return super.substring(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.2
|
||||
*/
|
||||
public synchronized StringBuffer insert(int index, char[] str, int offset,
|
||||
int len)
|
||||
{
|
||||
super.insert(index, str, offset, len);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized StringBuffer insert(int offset, Object obj) {
|
||||
super.insert(offset, String.valueOf(obj));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized StringBuffer insert(int offset, String str) {
|
||||
super.insert(offset, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized StringBuffer insert(int offset, char[] str) {
|
||||
super.insert(offset, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.5
|
||||
*/
|
||||
public StringBuffer insert(int dstOffset, CharSequence s) {
|
||||
// Note, synchronization achieved via other invocations
|
||||
if (s == null)
|
||||
s = "null";
|
||||
if (s instanceof String)
|
||||
return this.insert(dstOffset, (String)s);
|
||||
return this.insert(dstOffset, s, 0, s.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
* @since 1.5
|
||||
*/
|
||||
public synchronized StringBuffer insert(int dstOffset, CharSequence s,
|
||||
int start, int end)
|
||||
{
|
||||
super.insert(dstOffset, s, start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuffer insert(int offset, boolean b) {
|
||||
return insert(offset, String.valueOf(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public synchronized StringBuffer insert(int offset, char c) {
|
||||
super.insert(offset, c);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuffer insert(int offset, int i) {
|
||||
return insert(offset, String.valueOf(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuffer insert(int offset, long l) {
|
||||
return insert(offset, String.valueOf(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuffer insert(int offset, float f) {
|
||||
return insert(offset, String.valueOf(f));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuffer insert(int offset, double d) {
|
||||
return insert(offset, String.valueOf(d));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
* @since 1.4
|
||||
*/
|
||||
public int indexOf(String str) {
|
||||
return indexOf(str, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
* @since 1.4
|
||||
*/
|
||||
public synchronized int indexOf(String str, int fromIndex) {
|
||||
return String.indexOf(value, 0, count,
|
||||
str.toCharArray(), 0, str.length(), fromIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
* @since 1.4
|
||||
*/
|
||||
public int lastIndexOf(String str) {
|
||||
// Note, synchronization achieved via other invocations
|
||||
return lastIndexOf(str, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
* @since 1.4
|
||||
*/
|
||||
public synchronized int lastIndexOf(String str, int fromIndex) {
|
||||
return String.lastIndexOf(value, 0, count,
|
||||
str.toCharArray(), 0, str.length(), fromIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since JDK1.0.2
|
||||
*/
|
||||
public synchronized StringBuffer reverse() {
|
||||
super.reverse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized String toString() {
|
||||
return new String(value, 0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable fields for StringBuffer.
|
||||
*
|
||||
* @serialField value char[]
|
||||
* The backing character array of this StringBuffer.
|
||||
* @serialField count int
|
||||
* The number of characters in this StringBuffer.
|
||||
* @serialField shared boolean
|
||||
* A flag indicating whether the backing array is shared.
|
||||
* The value is ignored upon deserialization.
|
||||
*/
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields =
|
||||
{
|
||||
new java.io.ObjectStreamField("value", char[].class),
|
||||
new java.io.ObjectStreamField("count", Integer.TYPE),
|
||||
new java.io.ObjectStreamField("shared", Boolean.TYPE),
|
||||
};
|
||||
|
||||
/**
|
||||
* readObject is called to restore the state of the StringBuffer from
|
||||
* a stream.
|
||||
*/
|
||||
private synchronized void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException {
|
||||
java.io.ObjectOutputStream.PutField fields = s.putFields();
|
||||
fields.put("value", value);
|
||||
fields.put("count", count);
|
||||
fields.put("shared", false);
|
||||
s.writeFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* readObject is called to restore the state of the StringBuffer from
|
||||
* a stream.
|
||||
*/
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
java.io.ObjectInputStream.GetField fields = s.readFields();
|
||||
value = (char[])fields.get("value", null);
|
||||
count = fields.get("count", 0);
|
||||
}
|
||||
}
|
369
notes/src/StringBuilder.java
Normal file
369
notes/src/StringBuilder.java
Normal file
@ -0,0 +1,369 @@
|
||||
|
||||
package java.lang;
|
||||
|
||||
public final class StringBuilder
|
||||
extends AbstractStringBuilder
|
||||
implements java.io.Serializable, CharSequence
|
||||
{
|
||||
|
||||
/** use serialVersionUID for interoperability */
|
||||
static final long serialVersionUID = 4383685877147921099L;
|
||||
|
||||
/**
|
||||
* Constructs a string builder with no characters in it and an
|
||||
* initial capacity of 16 characters.
|
||||
*/
|
||||
public StringBuilder() {
|
||||
super(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string builder with no characters in it and an
|
||||
* initial capacity specified by the <code>capacity</code> argument.
|
||||
*
|
||||
* @param capacity the initial capacity.
|
||||
* @throws NegativeArraySizeException if the <code>capacity</code>
|
||||
* argument is less than <code>0</code>.
|
||||
*/
|
||||
public StringBuilder(int capacity) {
|
||||
super(capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string builder initialized to the contents of the
|
||||
* specified string. The initial capacity of the string builder is
|
||||
* <code>16</code> plus the length of the string argument.
|
||||
*
|
||||
* @param str the initial contents of the buffer.
|
||||
* @throws NullPointerException if <code>str</code> is <code>null</code>
|
||||
*/
|
||||
public StringBuilder(String str) {
|
||||
super(str.length() + 16);
|
||||
append(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a string builder that contains the same characters
|
||||
* as the specified <code>CharSequence</code>. The initial capacity of
|
||||
* the string builder is <code>16</code> plus the length of the
|
||||
* <code>CharSequence</code> argument.
|
||||
*
|
||||
* @param seq the sequence to copy.
|
||||
* @throws NullPointerException if <code>seq</code> is <code>null</code>
|
||||
*/
|
||||
public StringBuilder(CharSequence seq) {
|
||||
this(seq.length() + 16);
|
||||
append(seq);
|
||||
}
|
||||
|
||||
public StringBuilder append(Object obj) {
|
||||
return append(String.valueOf(obj));
|
||||
}
|
||||
|
||||
public StringBuilder append(String str) {
|
||||
super.append(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Appends the specified string builder to this sequence.
|
||||
private StringBuilder append(StringBuilder sb) {
|
||||
if (sb == null)
|
||||
return append("null");
|
||||
int len = sb.length();
|
||||
int newcount = count + len;
|
||||
if (newcount > value.length)
|
||||
expandCapacity(newcount);
|
||||
sb.getChars(0, len, value, count);
|
||||
count = newcount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the specified <tt>StringBuffer</tt> to this sequence.
|
||||
* <p>
|
||||
* The characters of the <tt>StringBuffer</tt> argument are appended,
|
||||
* in order, to this sequence, increasing the
|
||||
* length of this sequence by the length of the argument.
|
||||
* If <tt>sb</tt> is <tt>null</tt>, then the four characters
|
||||
* <tt>"null"</tt> are appended to this sequence.
|
||||
* <p>
|
||||
* Let <i>n</i> be the length of this character sequence just prior to
|
||||
* execution of the <tt>append</tt> method. Then the character at index
|
||||
* <i>k</i> in the new character sequence is equal to the character at
|
||||
* index <i>k</i> in the old character sequence, if <i>k</i> is less than
|
||||
* <i>n</i>; otherwise, it is equal to the character at index <i>k-n</i>
|
||||
* in the argument <code>sb</code>.
|
||||
*
|
||||
* @param sb the <tt>StringBuffer</tt> to append.
|
||||
* @return a reference to this object.
|
||||
*/
|
||||
public StringBuilder append(StringBuffer sb) {
|
||||
super.append(sb);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public StringBuilder append(CharSequence s) {
|
||||
if (s == null)
|
||||
s = "null";
|
||||
if (s instanceof String)
|
||||
return this.append((String)s);
|
||||
if (s instanceof StringBuffer)
|
||||
return this.append((StringBuffer)s);
|
||||
if (s instanceof StringBuilder)
|
||||
return this.append((StringBuilder)s);
|
||||
return this.append(s, 0, s.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder append(CharSequence s, int start, int end) {
|
||||
super.append(s, start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(char[] str) {
|
||||
super.append(str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder append(char[] str, int offset, int len) {
|
||||
super.append(str, offset, len);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(boolean b) {
|
||||
super.append(b);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(char c) {
|
||||
super.append(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(int i) {
|
||||
super.append(i);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(long lng) {
|
||||
super.append(lng);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(float f) {
|
||||
super.append(f);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringBuilder append(double d) {
|
||||
super.append(d);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
public StringBuilder appendCodePoint(int codePoint) {
|
||||
super.appendCodePoint(codePoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder delete(int start, int end) {
|
||||
super.delete(start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder deleteCharAt(int index) {
|
||||
super.deleteCharAt(index);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder replace(int start, int end, String str) {
|
||||
super.replace(start, end, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int index, char[] str, int offset,
|
||||
int len)
|
||||
{
|
||||
super.insert(index, str, offset, len);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, Object obj) {
|
||||
return insert(offset, String.valueOf(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, String str) {
|
||||
super.insert(offset, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, char[] str) {
|
||||
super.insert(offset, str);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int dstOffset, CharSequence s) {
|
||||
if (s == null)
|
||||
s = "null";
|
||||
if (s instanceof String)
|
||||
return this.insert(dstOffset, (String)s);
|
||||
return this.insert(dstOffset, s, 0, s.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int dstOffset, CharSequence s,
|
||||
int start, int end)
|
||||
{
|
||||
super.insert(dstOffset, s, start, end);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, boolean b) {
|
||||
super.insert(offset, b);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, char c) {
|
||||
super.insert(offset, c);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, int i) {
|
||||
return insert(offset, String.valueOf(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, long l) {
|
||||
return insert(offset, String.valueOf(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, float f) {
|
||||
return insert(offset, String.valueOf(f));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StringIndexOutOfBoundsException {@inheritDoc}
|
||||
*/
|
||||
public StringBuilder insert(int offset, double d) {
|
||||
return insert(offset, String.valueOf(d));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
*/
|
||||
public int indexOf(String str) {
|
||||
return indexOf(str, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
*/
|
||||
public int indexOf(String str, int fromIndex) {
|
||||
return String.indexOf(value, 0, count,
|
||||
str.toCharArray(), 0, str.length(), fromIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
*/
|
||||
public int lastIndexOf(String str) {
|
||||
return lastIndexOf(str, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NullPointerException {@inheritDoc}
|
||||
*/
|
||||
public int lastIndexOf(String str, int fromIndex) {
|
||||
return String.lastIndexOf(value, 0, count,
|
||||
str.toCharArray(), 0, str.length(), fromIndex);
|
||||
}
|
||||
|
||||
public StringBuilder reverse() {
|
||||
super.reverse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
// Create a copy, don't share the array
|
||||
return new String(value, 0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the state of the <tt>StringBuilder</tt> instance to a stream
|
||||
* (that is, serialize it).
|
||||
*
|
||||
* @serialData the number of characters currently stored in the string
|
||||
* builder (<tt>int</tt>), followed by the characters in the
|
||||
* string builder (<tt>char[]</tt>). The length of the
|
||||
* <tt>char</tt> array may be greater than the number of
|
||||
* characters currently stored in the string builder, in which
|
||||
* case extra characters are ignored.
|
||||
*/
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException {
|
||||
s.defaultWriteObject();
|
||||
s.writeInt(count);
|
||||
s.writeObject(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* readObject is called to restore the state of the StringBuffer from
|
||||
* a stream.
|
||||
*/
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
s.defaultReadObject();
|
||||
count = s.readInt();
|
||||
value = (char[]) s.readObject();
|
||||
}
|
||||
|
||||
}
|
@ -1,716 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 前言
|
||||
|
||||
省略的代码:
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
```
|
||||
|
||||
```java
|
||||
public class Solution {
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
Scanner in = new Scanner(System.in);
|
||||
while (in.hasNext()) {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 1. 小米-小米Git
|
||||
|
||||
- 重建多叉树
|
||||
- 使用 LCA
|
||||
|
||||
```java
|
||||
private class TreeNode {
|
||||
int id;
|
||||
List<TreeNode> childs = new ArrayList<>();
|
||||
|
||||
TreeNode(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSplitNode(String[] matrix, int indexA, int indexB) {
|
||||
int n = matrix.length;
|
||||
boolean[][] linked = new boolean[n][n]; // 重建邻接矩阵
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j < n; j++) {
|
||||
linked[i][j] = matrix[i].charAt(j) == '1';
|
||||
}
|
||||
}
|
||||
TreeNode tree = constructTree(linked, 0);
|
||||
TreeNode ancestor = LCA(tree, new TreeNode(indexA), new TreeNode(indexB));
|
||||
return ancestor.id;
|
||||
}
|
||||
|
||||
private TreeNode constructTree(boolean[][] linked, int root) {
|
||||
TreeNode tree = new TreeNode(root);
|
||||
for (int i = 0; i < linked[root].length; i++) {
|
||||
if (linked[root][i]) {
|
||||
linked[i][root] = false; // 因为题目给的邻接矩阵是双向的,在这里需要把它转为单向的
|
||||
tree.childs.add(constructTree(links, i));
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
private TreeNode LCA(TreeNode root, TreeNode p, TreeNode q) {
|
||||
if (root == null || root.id == p.id || root.id == q.id) return root;
|
||||
TreeNode ancestor = null;
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < root.childs.size(); i++) {
|
||||
TreeNode tmp = LCA(root.childs.get(i), p, q);
|
||||
if (tmp != null) {
|
||||
ancestor = tmp;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
return cnt == 2 ? root : ancestor;
|
||||
}
|
||||
```
|
||||
|
||||
# 2. 小米-懂二进制
|
||||
|
||||
对两个数进行异或,结果的二进制表示为 1 的那一位就是两个数不同的位。
|
||||
|
||||
```java
|
||||
public int countBitDiff(int m, int n) {
|
||||
return Integer.bitCount(m ^ n);
|
||||
}
|
||||
```
|
||||
|
||||
# 3. 小米-中国牛市
|
||||
|
||||
背包问题,可以设一个大小为 2 的背包。
|
||||
|
||||
状态转移方程如下:
|
||||
|
||||
```html
|
||||
dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
|
||||
```
|
||||
|
||||
```java
|
||||
public int calculateMax(int[] prices) {
|
||||
int n = prices.length;
|
||||
int[][] dp = new int[3][n];
|
||||
for (int i = 1; i <= 2; i++) {
|
||||
int localMax = dp[i - 1][0] - prices[0];
|
||||
for (int j = 1; j < n; j++) {
|
||||
dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax);
|
||||
localMax = Math.max(localMax, dp[i - 1][j] - prices[j]);
|
||||
}
|
||||
}
|
||||
return dp[2][n - 1];
|
||||
}
|
||||
```
|
||||
|
||||
# 4. 微软-LUCKY STRING
|
||||
|
||||
- 斐波那契数列可以预计算;
|
||||
- 从头到尾遍历字符串的过程,每一轮循环都使用一个 Set 来保存从 i 到 j 出现的字符,并且 Set 保证了字符都不同,因此 Set 的大小就是不同字符的个数。
|
||||
|
||||
```java
|
||||
Set<Integer> fibSet = new HashSet<>(Arrays.asList(1, 2, 3, 5, 8, 13, 21, 34, 55, 89));
|
||||
Scanner in = new Scanner(System.in);
|
||||
String str = in.nextLine();
|
||||
int n = str.length();
|
||||
Set<String> ret = new HashSet<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Set<Character> set = new HashSet<>();
|
||||
for (int j = i; j < n; j++) {
|
||||
set.add(str.charAt(j));
|
||||
int cnt = set.size();
|
||||
if (fibSet.contains(cnt)) {
|
||||
ret.add(str.substring(i, j + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
String[] arr = ret.toArray(new String[ret.size()]);
|
||||
Arrays.sort(arr);
|
||||
for (String s : arr) {
|
||||
System.out.println(s);
|
||||
}
|
||||
```
|
||||
|
||||
# 5. 微软-Numeric Keypad
|
||||
|
||||
```java
|
||||
private static int[][] canReach = {
|
||||
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 0
|
||||
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 1
|
||||
{1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, // 2
|
||||
{0, 0, 0, 1, 0, 0, 1, 0, 0, 1}, // 3
|
||||
{1, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 4
|
||||
{1, 0, 0, 0, 0, 1, 1, 0, 1, 1}, // 5
|
||||
{0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, // 6
|
||||
{1, 0, 0, 0, 0, 0, 0, 1, 1, 1}, // 7
|
||||
{1, 0, 0, 0, 0, 0, 0, 0, 1, 1}, // 8
|
||||
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 9
|
||||
};
|
||||
|
||||
private static boolean isLegal(char[] chars, int idx) {
|
||||
if (idx >= chars.length || idx < 0) return true;
|
||||
int cur = chars[idx] - '0';
|
||||
int next = chars[idx + 1] - '0';
|
||||
return canReach[cur][next] == 1;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Scanner in = new Scanner(System.in);
|
||||
int T = Integer.valueOf(in.nextLine());
|
||||
for (int i = 0; i < T; i++) {
|
||||
String line = in.nextLine();
|
||||
char[] chars = line.toCharArray();
|
||||
for (int j = 0; j < chars.length - 1; j++) {
|
||||
while (!isLegal(chars, j)) {
|
||||
if (--chars[j + 1] < '0') {
|
||||
chars[j--]--;
|
||||
}
|
||||
for (int k = j + 2; k < chars.length; k++) {
|
||||
chars[k] = '9';
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println(new String(chars));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 6. 微软-Spring Outing
|
||||
|
||||
下面以 N = 3,K = 4 来进行讨论。
|
||||
|
||||
初始时,令第 0 个地方成为待定地点,也就是呆在家里。
|
||||
|
||||
从第 4 个地点开始投票,每个人只需要比较第 4 个地方和第 0 个地方的优先级,里,如果超过半数的人选择了第 4 个地方,那么更新第 4 个地方成为待定地点。
|
||||
|
||||
从后往前不断重复以上步骤,不断更新待定地点,直到所有地方都已经投票。
|
||||
|
||||
上面的讨论中,先令第 0 个地点成为待定地点,是因为这样的话第 4 个地点就只需要和这个地点进行比较,而不用考虑其它情况。如果最开始先令第 1 个地点成为待定地点,那么在对第 2 个地点进行投票时,每个人不仅要考虑第 2 个地点与第 1 个地点的优先级,也要考虑与其后投票地点的优先级。
|
||||
|
||||
```java
|
||||
int N = in.nextInt();
|
||||
int K = in.nextInt();
|
||||
int[][] votes = new int[N][K + 1];
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int j = 0; j < K + 1; j++) {
|
||||
int place = in.nextInt();
|
||||
votes[i][place] = j;
|
||||
}
|
||||
}
|
||||
int ret = 0;
|
||||
for (int place = K; place > 0; place--) {
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < N; i++) {
|
||||
if (votes[i][place] < votes[i][ret]) {
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
if (cnt > N / 2) {
|
||||
ret = place;
|
||||
}
|
||||
}
|
||||
System.out.println(ret == 0 ? "otaku" : ret);
|
||||
```
|
||||
|
||||
# 7. 微软-S-expression
|
||||
|
||||
# 8. 华为-最高分是多少
|
||||
|
||||
```java
|
||||
int N = in.nextInt();
|
||||
int M = in.nextInt();
|
||||
int[] scores = new int[N];
|
||||
for (int i = 0; i < N; i++) {
|
||||
scores[i] = in.nextInt();
|
||||
}
|
||||
for (int i = 0; i < M; i++) {
|
||||
String str = in.next();
|
||||
if (str.equals("U")) {
|
||||
int id = in.nextInt() - 1;
|
||||
int newScore = in.nextInt();
|
||||
scores[id] = newScore;
|
||||
} else {
|
||||
int idBegin = in.nextInt() - 1;
|
||||
int idEnd = in.nextInt() - 1;
|
||||
int ret = 0;
|
||||
if (idBegin > idEnd) {
|
||||
int t = idBegin;
|
||||
idBegin = idEnd;
|
||||
idEnd = t;
|
||||
}
|
||||
for (int j = idBegin; j <= idEnd; j++) {
|
||||
ret = Math.max(ret, scores[j]);
|
||||
}
|
||||
System.out.println(ret);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 9. 华为-简单错误记录
|
||||
|
||||
```java
|
||||
HashMap<String, Integer> map = new LinkedHashMap<>();
|
||||
while (in.hasNextLine()) {
|
||||
String s = in.nextLine();
|
||||
String key = s.substring(s.lastIndexOf('\\') + 1);
|
||||
map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1);
|
||||
}
|
||||
List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet());
|
||||
Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue());
|
||||
for (int i = 0; i < 8 && i < list.size(); i++) {
|
||||
String[] token = list.get(i).getKey().split(" ");
|
||||
String filename = token[0];
|
||||
String line = token[1];
|
||||
if (filename.length() > 16) filename = filename.substring(filename.length() - 16);
|
||||
System.out.println(filename + " " + line + " " + list.get(i).getValue());
|
||||
}
|
||||
```
|
||||
|
||||
# 10. 华为-扑克牌大小
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
|
||||
private Map<String, Integer> map = new HashMap<>();
|
||||
|
||||
public Main() {
|
||||
map.put("3", 0);
|
||||
map.put("4", 1);
|
||||
map.put("5", 2);
|
||||
map.put("6", 3);
|
||||
map.put("7", 4);
|
||||
map.put("8", 5);
|
||||
map.put("9", 6);
|
||||
map.put("10", 7);
|
||||
map.put("J", 8);
|
||||
map.put("Q", 9);
|
||||
map.put("K", 10);
|
||||
map.put("A", 11);
|
||||
map.put("2", 12);
|
||||
map.put("joker", 13);
|
||||
map.put("JOKER ", 14);
|
||||
}
|
||||
|
||||
private String play(String s1, String s2) {
|
||||
String[] token1 = s1.split(" ");
|
||||
String[] token2 = s2.split(" ");
|
||||
CardType type1 = computeCardType(token1);
|
||||
CardType type2 = computeCardType(token2);
|
||||
if (type1 == CardType.DoubleJoker) return s1;
|
||||
if (type2 == CardType.DoubleJoker) return s2;
|
||||
if (type1 == CardType.Bomb && type2 != CardType.Bomb) return s1;
|
||||
if (type2 == CardType.Bomb && type1 != CardType.Bomb) return s2;
|
||||
if (type1 != type2 || token1.length != token2.length) return "ERROR";
|
||||
for (int i = 0; i < token1.length; i++) {
|
||||
int val1 = map.get(token1[i]);
|
||||
int val2 = map.get(token2[i]);
|
||||
if (val1 != val2) return val1 > val2 ? s1 : s2;
|
||||
}
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
private CardType computeCardType(String[] token) {
|
||||
boolean hasjoker = false, hasJOKER = false;
|
||||
for (int i = 0; i < token.length; i++) {
|
||||
if (token[i].equals("joker")) hasjoker = true;
|
||||
else if (token[i].equals("JOKER")) hasJOKER = true;
|
||||
}
|
||||
if (hasjoker && hasJOKER) return CardType.DoubleJoker;
|
||||
int maxContinueLen = 1;
|
||||
int curContinueLen = 1;
|
||||
String curValue = token[0];
|
||||
for (int i = 1; i < token.length; i++) {
|
||||
if (token[i].equals(curValue)) curContinueLen++;
|
||||
else {
|
||||
curContinueLen = 1;
|
||||
curValue = token[i];
|
||||
}
|
||||
maxContinueLen = Math.max(maxContinueLen, curContinueLen);
|
||||
}
|
||||
if (maxContinueLen == 4) return CardType.Bomb;
|
||||
if (maxContinueLen == 3) return CardType.Triple;
|
||||
if (maxContinueLen == 2) return CardType.Double;
|
||||
boolean isStraight = true;
|
||||
for (int i = 1; i < token.length; i++) {
|
||||
if (map.get(token[i]) - map.get(token[i - 1]) != 1) {
|
||||
isStraight = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isStraight && token.length == 5) return CardType.Straight;
|
||||
return CardType.Sigal;
|
||||
}
|
||||
|
||||
private enum CardType {
|
||||
DoubleJoker, Bomb, Sigal, Double, Triple, Straight;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Main main = new Main();
|
||||
Scanner in = new Scanner(System.in);
|
||||
while (in.hasNextLine()) {
|
||||
String s = in.nextLine();
|
||||
String[] token = s.split("-");
|
||||
System.out.println(main.play(token[0], token[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 11. 去哪儿-二分查找
|
||||
|
||||
对于有重复元素的有序数组,二分查找需要注意以下要点:
|
||||
|
||||
- if (val <= A[m]) h = m;
|
||||
- 因为 h 的赋值为 m 而不是 m - 1,因此 while 循环的条件也就为 l < h。(如果是 m - 1 循环条件为 l <= h)
|
||||
|
||||
```java
|
||||
public int getPos(int[] A, int n, int val) {
|
||||
int l = 0, h = n - 1;
|
||||
while (l < h) {
|
||||
int m = l + (h - l) / 2;
|
||||
if (val <= A[m]) h = m;
|
||||
else l = m + 1;
|
||||
}
|
||||
return A[h] == val ? h : -1;
|
||||
}
|
||||
```
|
||||
|
||||
# 12. 去哪儿-首个重复字符
|
||||
|
||||
```java
|
||||
public char findFirstRepeat(String A, int n) {
|
||||
boolean[] hasAppear = new boolean[256];
|
||||
for (int i = 0; i < n; i++) {
|
||||
char c = A.charAt(i);
|
||||
if(hasAppear[c]) return c;
|
||||
hasAppear[c] = true;
|
||||
}
|
||||
return ' ';
|
||||
}
|
||||
```
|
||||
|
||||
# 13. 去哪儿-寻找Coder
|
||||
|
||||
```java
|
||||
public String[] findCoder(String[] A, int n) {
|
||||
List<Pair<String, Integer>> list = new ArrayList<>();
|
||||
for (String s : A) {
|
||||
int cnt = 0;
|
||||
String t = s.toLowerCase();
|
||||
int idx = -1;
|
||||
while (true) {
|
||||
idx = t.indexOf("coder", idx + 1);
|
||||
if (idx == -1) break;
|
||||
cnt++;
|
||||
}
|
||||
if (cnt != 0) {
|
||||
list.add(new Pair<>(s, cnt));
|
||||
}
|
||||
}
|
||||
Collections.sort(list, (o1, o2) -> (o2.getValue() - o1.getValue()));
|
||||
String[] ret = new String[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
ret[i] = list.get(i).getKey();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// 牛客网无法导入 javafx.util.Pair,这里就自己实现一下 Pair 类
|
||||
private class Pair<T, K> {
|
||||
T t;
|
||||
K k;
|
||||
|
||||
Pair(T t, K k) {
|
||||
this.t = t;
|
||||
this.k = k;
|
||||
}
|
||||
|
||||
T getKey() {
|
||||
return t;
|
||||
}
|
||||
|
||||
K getValue() {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 14. 美团-最大差值
|
||||
|
||||
贪心策略。
|
||||
|
||||
```java
|
||||
public int getDis(int[] A, int n) {
|
||||
int max = 0;
|
||||
int soFarMin = A[0];
|
||||
for (int i = 1; i < n; i++) {
|
||||
if(soFarMin > A[i]) soFarMin = A[i];
|
||||
else max = Math.max(max, A[i]- soFarMin);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
```
|
||||
|
||||
# 15. 美团-棋子翻转
|
||||
|
||||
```java
|
||||
public int[][] flipChess(int[][] A, int[][] f) {
|
||||
int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
|
||||
for (int[] ff : f) {
|
||||
for (int[] dd : direction) {
|
||||
int r = ff[0] + dd[0] - 1, c = ff[1] + dd[1] - 1;
|
||||
if(r < 0 || r > 3 || c < 0 || c > 3) continue;
|
||||
A[r][c] ^= 1;
|
||||
}
|
||||
}
|
||||
return A;
|
||||
}
|
||||
```
|
||||
|
||||
# 16. 美团-拜访
|
||||
|
||||
```java
|
||||
private Set<String> paths;
|
||||
private List<Integer> curPath;
|
||||
|
||||
public int countPath(int[][] map, int n, int m) {
|
||||
paths = new HashSet<>();
|
||||
curPath = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = 0; j < m; j++) {
|
||||
if (map[i][j] == 1) {
|
||||
map[i][j] = -1;
|
||||
int[][] leftRightDirection = {{1, 0}, {-1, 0}};
|
||||
int[][] topDownDirection = {{0, 1}, {0, -1}};
|
||||
for (int[] lr : leftRightDirection) {
|
||||
for (int[] td : topDownDirection) {
|
||||
int[][] directions = {lr, td};
|
||||
backtracking(map, n, m, i, j, directions);
|
||||
}
|
||||
}
|
||||
return paths.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void backtracking(int[][] map, int n, int m, int r, int c, int[][] directions) {
|
||||
if (map[r][c] == 2) {
|
||||
String path = "";
|
||||
for (int num : curPath) {
|
||||
path += num;
|
||||
}
|
||||
paths.add(path);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < directions.length; i++) {
|
||||
int nextR = r + directions[i][0];
|
||||
int nextC = c + directions[i][1];
|
||||
if (nextR < 0 || nextR >= n || nextC < 0 || nextC >= m || map[nextR][nextC] == -1) continue;
|
||||
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : -1;
|
||||
curPath.add(nextR);
|
||||
curPath.add(nextC);
|
||||
backtracking(map, n, m, nextR, nextC, directions);
|
||||
curPath.remove(curPath.size() - 1);
|
||||
curPath.remove(curPath.size() - 1);
|
||||
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 17. 美团-直方图内最大矩形
|
||||
|
||||
```java
|
||||
public int countArea(int[] A, int n) {
|
||||
int max = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int min = A[i];
|
||||
for (int j = i; j < n; j++) {
|
||||
min = Math.min(min, A[j]);
|
||||
max = Math.max(max, min * (j - i + 1));
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
```
|
||||
|
||||
# 18. 美团-字符串计数
|
||||
|
||||
字符串都是小写字符,可以把字符串当成是 26 进制。但是字典序的比较和普通的整数比较不同,是从左往右进行比较,例如 "ac" 和 "abc",字典序的比较结果为 "ac" > "abc",如果按照整数方法比较,因为 "abc" 是三位数,显然更大。
|
||||
|
||||
由于两个字符串的长度可能不想等,在 s1 空白部分和 s2 对应部分进行比较时,应该把 s1 的空白部分看成是 'a' 字符进行填充的。
|
||||
|
||||
还有一点要注意的是,s1 到 s2 长度为 len<sub>i</sub> 的字符串个数只比较前面 i 个字符。例如 'aaa' 和 'bbb' ,长度为 2 的个数为 'aa' 到 'bb' 的字符串个数,不需要考虑后面部分的字符。
|
||||
|
||||
在统计个数时,从 len1 开始一直遍历到最大合法长度,每次循环都统计长度为 i 的子字符串个数。
|
||||
|
||||
```java
|
||||
String s1 = in.next();
|
||||
String s2 = in.next();
|
||||
int len1 = in.nextInt();
|
||||
int len2 = in.nextInt();
|
||||
int len = Math.min(s2.length(), len2);
|
||||
int[] subtractArr = new int[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c1 = i < s1.length() ? s1.charAt(i) : 'a';
|
||||
char c2 = s2.charAt(i);
|
||||
subtractArr[i] = c2 - c1;
|
||||
}
|
||||
int ret = 0;
|
||||
for (int i = len1; i <= len; i++) {
|
||||
for (int j = 0; j < i; j++) {
|
||||
ret += subtractArr[j] * Math.pow(26, i - j - 1);
|
||||
}
|
||||
}
|
||||
System.out.println(ret - 1);
|
||||
```
|
||||
|
||||
# 19. 美团-平均年龄
|
||||
|
||||
```java
|
||||
int W = in.nextInt();
|
||||
double Y = in.nextDouble();
|
||||
double x = in.nextDouble();
|
||||
int N = in.nextInt();
|
||||
while (N-- > 0) {
|
||||
Y++; // 老员工每年年龄都要加 1
|
||||
Y += (21 - Y) * x;
|
||||
}
|
||||
System.out.println((int) Math.ceil(Y));
|
||||
```
|
||||
|
||||
# 20. 百度-罪犯转移
|
||||
|
||||
部分和问题,将每次求的部分和缓存起来。
|
||||
|
||||
```java
|
||||
int n = in.nextInt();
|
||||
int t = in.nextInt();
|
||||
int c = in.nextInt();
|
||||
int[] values = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
values[i] = in.nextInt();
|
||||
}
|
||||
int cnt = 0;
|
||||
int totalValue = 0;
|
||||
for (int s = 0, e = c - 1; e < n; s++, e++) {
|
||||
if (s == 0) {
|
||||
for (int j = 0; j < c; j++) totalValue += values[j];
|
||||
} else {
|
||||
totalValue = totalValue - values[s - 1] + values[e];
|
||||
}
|
||||
if (totalValue <= t) cnt++;
|
||||
}
|
||||
System.out.println(cnt);
|
||||
```
|
||||
|
||||
# 22. 百度-裁减网格纸
|
||||
|
||||
```java
|
||||
int n = in.nextInt();
|
||||
int minX, minY, maxX, maxY;
|
||||
minX = minY = Integer.MAX_VALUE;
|
||||
maxX = maxY = Integer.MIN_VALUE;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int x = in.nextInt();
|
||||
int y = in.nextInt();
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
System.out.println((int) Math.pow(Math.max(maxX - minX, maxY - minY), 2));
|
||||
```
|
||||
|
||||
# 23. 百度-钓鱼比赛
|
||||
|
||||
P ( 至少钓一条鱼 ) = 1 - P ( 一条也钓不到 )
|
||||
|
||||
坑:读取概率矩阵的时候,需要一行一行进行读取,而不能直接用 in.nextDouble()。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
Scanner in = new Scanner(System.in);
|
||||
while (in.hasNext()) {
|
||||
int n = in.nextInt();
|
||||
int m = in.nextInt();
|
||||
int x = in.nextInt();
|
||||
int y = in.nextInt();
|
||||
int t = in.nextInt();
|
||||
in.nextLine(); // 坑
|
||||
double pcc = 0.0;
|
||||
double sum = 0.0;
|
||||
for (int i = 1; i <= n; i++) {
|
||||
String[] token = in.nextLine().split(" "); // 坑
|
||||
for (int j = 1; j <= m; j++) {
|
||||
double p = Double.parseDouble(token[j - 1]);
|
||||
// double p = in.nextDouble();
|
||||
sum += p;
|
||||
if (i == x && j == y) {
|
||||
pcc = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
double pss = sum / (n * m);
|
||||
pcc = computePOfIRT(pcc, t);
|
||||
pss = computePOfIRT(pss, t);
|
||||
System.out.println(pcc > pss ? "cc" : pss > pcc ? "ss" : "equal");
|
||||
System.out.printf("%.2f\n", Math.max(pcc, pss));
|
||||
}
|
||||
}
|
||||
|
||||
// compute probability of independent repeated trials
|
||||
private static double computePOfIRT(double p, int t) {
|
||||
return 1 - Math.pow((1 - p), t);
|
||||
}
|
||||
```
|
||||
|
||||
# 24. 百度-蘑菇阵
|
||||
|
||||
这题用回溯会超时,需要用 DP。
|
||||
|
||||
dp[i][j] 表示到达 (i,j) 位置不会触碰蘑菇的概率。对于 N\*M 矩阵,如果 i == N || j == M,那么 (i,j) 只能有一个移动方向;其它情况下能有两个移动方向。
|
||||
|
||||
考虑以下矩阵,其中第 3 行和第 3 列只能往一个方向移动,而其它位置可以有两个方向移动。
|
||||
|
||||
|
||||
```java
|
||||
int N = in.nextInt();
|
||||
int M = in.nextInt();
|
||||
int K = in.nextInt();
|
||||
boolean[][] mushroom = new boolean[N][M];
|
||||
while (K-- > 0) {
|
||||
int x = in.nextInt();
|
||||
int y = in.nextInt();
|
||||
mushroom[x - 1][y - 1] = true;
|
||||
}
|
||||
double[][] dp = new double[N][M];
|
||||
dp[0][0] = 1;
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int j = 0; j < M; j++) {
|
||||
if (mushroom[i][j]) dp[i][j] = 0;
|
||||
else {
|
||||
double cur = dp[i][j];
|
||||
if (i == N - 1 && j == M - 1) break;
|
||||
if (i == N - 1) dp[i][j + 1] += cur;
|
||||
else if (j == M - 1) dp[i + 1][j] += cur;
|
||||
else {
|
||||
dp[i][j + 1] += cur / 2;
|
||||
dp[i + 1][j] += cur / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.printf("%.2f\n", dp[N - 1][M - 1]);
|
||||
```
|
@ -1 +0,0 @@
|
||||
|
@ -1,339 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# const 关键字
|
||||
|
||||
用于定义常量,一旦初始化之后就无法改变。
|
||||
|
||||
常量必须在定义的时候进行初始化,因此用 const 修饰成员变量,必须在构造函数列表中初始化。
|
||||
|
||||
const 修饰指针时,分为顶层 const 和底层 const。
|
||||
|
||||
顶层 const 表示指针本身是个常量。
|
||||
|
||||
```c++
|
||||
int i = 0;
|
||||
int* const p = &i;
|
||||
```
|
||||
|
||||
底层 const 表示指针所指向的对象时一个常量。
|
||||
|
||||
```c++
|
||||
const int i = 0;
|
||||
const int* p = &i;
|
||||
```
|
||||
|
||||
const 修饰成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改变。
|
||||
|
||||
# static 关键字
|
||||
|
||||
[static详解](http://blog.csdn.net/shanghairuoxiao/article/details/72904292)
|
||||
|
||||
# extern 关键字
|
||||
|
||||
[C/C++中extern关键字详解](http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html)
|
||||
|
||||
# volatile 关键字
|
||||
|
||||
关键字volatile的作用是指示编译器,即使代码不对变量做任何改动,该变量的值仍可能会被外界修改。操作系统、硬件或其他线程都有可能修改该变量。该变量的值有可能遭受意料之外的修改,因此,每一次使用时,编译器都会重新从内存中获取这个值,而不是从寄存器中获取。
|
||||
|
||||
volatile变量不会被优化掉,这非常有用。设想有下面这个函数:
|
||||
|
||||
```c++
|
||||
int opt = 1;
|
||||
void Fn(void) {
|
||||
start:
|
||||
if (opt == 1) goto start;
|
||||
else break;
|
||||
}
|
||||
```
|
||||
|
||||
乍一看,上面的代码好像会进入无限循环,编译器可能会将这段代码优化成:
|
||||
```c++
|
||||
void Fn(void) {
|
||||
start:
|
||||
int opt = 1;
|
||||
if (true)
|
||||
goto start;
|
||||
}
|
||||
```
|
||||
|
||||
这样就变成了无限循环。然后,外部操作可能会将 0 写入变量 opt 的位置,从而终止循环。
|
||||
|
||||
为了防止编译器执行这类优化,我们需要设法通知编译器,系统其他部分可能会修改这个变量。具体做法就是使用 volatile 关键字,
|
||||
|
||||
# new 与 maclloc 区别
|
||||
|
||||
1. new 分配内存按照数据类型进行分配,malloc 分配内存按照大小分配;
|
||||
2. new 不仅分配一段内存,而且会调用构造函数,但是 malloc 则不会。
|
||||
3. new 返回的是指定对象的指针,而 malloc 返回的是 void\*,因此 malloc 的返回值一般都需要进行类型转化;
|
||||
4. new 是一个操作符可以重载,malloc 是一个库函数;
|
||||
5. new 分配的内存要用 delete 销毁,malloc 要用 free 来销毁;delete 销毁的时候会调用对象的析构函数,而 free 则不会;
|
||||
6. malloc 分配的内存不够的时候,可以用 realloc 扩容。扩容的原理?new 没用这样操作;
|
||||
7. new 如果分配失败了会抛出 bad_malloc 的异常,而 malloc 失败了会返回 NULL。因此对于 new,正确的姿势是采用 try...catch 语法,而 malloc 则应该判断指针的返回值。为了兼容很多 c 程序员的习惯,C++ 也可以采用 new nothrow 的方法禁止抛出异常而返回 NULL;
|
||||
8. new 和 new[] 的区别,new[] 一次分配所有内存,多次调用构造函数,分别搭配使用 delete 和 delete[],同理,delete[] 多次调用析构函数,销毁数组中的每个对象。而 malloc 则只能 sizeof(int) * n;
|
||||
9. 如果不够可以继续谈 new 和 malloc 的实现,空闲链表,分配方法 ( 首次适配原则,最佳适配原则,最差适配原则,快速适配原则 )。delete 和 free 的实现原理,free 为什么直到销毁多大的空间?
|
||||
|
||||
# 四种类型转换
|
||||
|
||||
**const_cast** 用于将 const 变量转为非 const
|
||||
|
||||
**static_cast** 用的最多,对于各种隐式转换,非 const 转 const,void\* 转指针等 , static_cast 能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
|
||||
|
||||
**dynamic_cast** 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。要深入了解内部转换的原理。
|
||||
|
||||
**reinterpret_cast** 几乎什么都可以转,比如将 int 转指针,可能会出问题,尽量少用;
|
||||
|
||||
为什么不使用 C 的强制转换?C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
|
||||
|
||||
# 指针和引用的区别
|
||||
|
||||
指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
|
||||
|
||||
指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
|
||||
|
||||
引用在定义的时候必须初始化,而指针则不需要;
|
||||
|
||||
指针有指向常量的指针和指针常量,而引用没有常量引用;
|
||||
|
||||
指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。
|
||||
|
||||
# 指针和数组的联系
|
||||
|
||||
一个一维 int 数组的数组名实际上是一个 int\* const 类型;
|
||||
一个二维 int 数组的数组名实际上是一个 int (\*const p)[n];
|
||||
数组名做参数会退化为指针
|
||||
|
||||
# 智能指针
|
||||
|
||||
构造函数中计数初始化为1;
|
||||
拷贝构造函数中计数值加1;
|
||||
赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
|
||||
析构函数中引用计数减一;
|
||||
在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
|
||||
share_prt与weak_ptr的区别?
|
||||
|
||||
```c++
|
||||
//share_ptr可能出现循环引用,从而导致内存泄露
|
||||
class A
|
||||
{
|
||||
public:
|
||||
share_ptr p;
|
||||
};
|
||||
class B
|
||||
{
|
||||
public:
|
||||
share_ptr p;
|
||||
}
|
||||
int main()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
share_prt pa(new A()); //pa的引用计数初始化为1
|
||||
share_prt pb(new B()); //pb的引用计数初始化为1
|
||||
pa->p = pb; //pb的引用计数变为2
|
||||
pb->p = pa; //pa的引用计数变为2
|
||||
}
|
||||
//假设pa先离开,引用计数减一变为1,不为0因此不会调用class A的析构函数,因此其成员p也不会被析构,pb的引用计数仍然为2;
|
||||
//同理pb离开的时候,引用计数也不能减到0
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
** weak_ptr是一种弱引用指针,其存在不会影响引用计数,从而解决循环引用的问题
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
# 多态性
|
||||
|
||||
作者:oscarwin
|
||||
链接:https://www.nowcoder.com/discuss/59394
|
||||
来源:牛客网
|
||||
|
||||
C++多态性与虚函数表
|
||||
|
||||
C++多态的实现?
|
||||
多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
|
||||
动态多态实现有几个条件:
|
||||
(1) 虚函数;
|
||||
(2) 一个基类的指针或引用指向派生类的对象;
|
||||
基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
|
||||
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
|
||||
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
|
||||
虚函数的作用?
|
||||
|
||||
虚函数用于实现多态,这点大家都能答上来
|
||||
但是虚函数在设计上还具有封装和抽象的作用。比如抽象工厂模式。
|
||||
动态绑定是如何实现的?
|
||||
第一个问题中基本回答了,主要都是结合虚函数表来答就行。
|
||||
|
||||
静态多态和动态多态。静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。
|
||||
|
||||
虚函数表
|
||||
|
||||
虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?
|
||||
|
||||
编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。
|
||||
纯虚函数如何定义,为什么对于存在虚函数的类中析构函数要定义成虚函数
|
||||
为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。
|
||||
|
||||
1
|
||||
2
|
||||
//纯虚函数定义
|
||||
virtual ~myClass() = 0;
|
||||
析构函数能抛出异常吗
|
||||
答案肯定是不能。
|
||||
|
||||
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
|
||||
|
||||
(1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
|
||||
|
||||
(2) 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
|
||||
|
||||
构造函数和析构函数中调用虚函数吗?
|
||||
|
||||
|
||||
# 内存对齐
|
||||
|
||||
作者:oscarwin
|
||||
链接:https://www.nowcoder.com/discuss/59394
|
||||
来源:牛客网
|
||||
|
||||
从0位置开始存储;
|
||||
变量存储的起始位置是该变量大小的整数倍;
|
||||
结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
|
||||
结构体中包含结构体,从结构体中最大元素的整数倍开始存;
|
||||
如果加入pragma pack(n) ,取n和变量自身大小较小的一个。
|
||||
|
||||
# 内联函数
|
||||
|
||||
宏定义在预编译的时候就会进行宏替换;
|
||||
|
||||
内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
|
||||
|
||||
内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
|
||||
|
||||
使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a b,这很危险,正确写法:#define MUL(a, b) ((a) (b))
|
||||
|
||||
# 内存管理
|
||||
|
||||
C++ 内存分为那几块?(堆区,栈区,常量区,静态和全局区)
|
||||
每块存储哪些变量?
|
||||
学会迁移,可以说到 malloc,从 malloc 说到操作系统的内存管理,说道内核态和用户态,然后就什么高端内存,slab 层,伙伴算法,VMA 可以巴拉巴拉了,接着可以迁移到 fork()。
|
||||
|
||||
# STL 的内存池实现
|
||||
|
||||
STL 内存分配分为一级分配器和二级分配器,一级分配器就是采用 malloc 分配内存,二级分配器采用内存池。
|
||||
|
||||
二级分配器设计的非常巧妙,分别给 8k,16k,..., 128k 等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块 10K 的内存,那么就找到最小的大于等于 10k 的块,也就是 16K,从16K 的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。如果要分配的内存大于 128K 则直接调用一级分配器。
|
||||
|
||||
为了节省维持链表的开销,采用了一个 union 结构体,分配器使用 union 里的 next 指针来指向下一个节点,而用户则使用 union 的空指针来表示该节点的地址。
|
||||
|
||||
# STL 里的 set 和 map 实现
|
||||
|
||||
set 和 map 都是基于红黑树实现的。
|
||||
|
||||
红黑树是一种平衡二叉查找树,AVL 树是完全平衡的,红黑树基本上是平衡的。
|
||||
|
||||
与 AVL 相比红黑数插入和删除最多只需要 3 次旋转,而 AVL 树为了维持其完全平衡性,在坏的情况下要旋转的次数太多。
|
||||
|
||||
# 必须在构造函数初始化列表里进行初始化的数据成员
|
||||
|
||||
1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面;
|
||||
2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面;
|
||||
3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
|
||||
|
||||
# 手写 strcpy()
|
||||
|
||||
```c++
|
||||
char* strcpy(char* dest, const char* src)
|
||||
{
|
||||
char *save = dest;
|
||||
while (*dest++ = *src++){}
|
||||
return save;
|
||||
}
|
||||
```
|
||||
|
||||
```c++
|
||||
char src[] = "abc";
|
||||
char dest[10]; // dest 必须大于 src
|
||||
strcpy(dest, src);
|
||||
cout << dest << endl;
|
||||
```
|
||||
|
||||
# 手写 strcat()
|
||||
|
||||
```
|
||||
char* strcat(char* dest, const char* src)
|
||||
{
|
||||
char* save = dest;
|
||||
while (*dest) dest++;
|
||||
while(*dest++ = *src++){}
|
||||
return save;
|
||||
}
|
||||
```
|
||||
|
||||
# 手写 strcmp()
|
||||
|
||||
```c++
|
||||
int strcmp(const char* s1, const char* s2)
|
||||
{
|
||||
while(*s1 == *s2 && *s1)
|
||||
{
|
||||
s1++;
|
||||
s2++;
|
||||
}
|
||||
return *s1 - *s2;
|
||||
}
|
||||
```
|
||||
|
||||
# 手写 strstr()
|
||||
|
||||
要求不能用其它库函数。
|
||||
|
||||
直接使用暴力方法,每次比较 src 的一部分是否与 target 相等。
|
||||
|
||||
循环体只需要执行 N - M + 1 次即可,由于不能用 strlen() ,因此无法直接计算出 N 与 M,可以使用一个额外的指针,令它先从 src 头部移动 M - 1 次,这样它还可以再移动的次数就为 N - M + 1 次。
|
||||
|
||||
```c++
|
||||
char* strstr(const char* src, const char* target)
|
||||
{
|
||||
if (!*target) return (char*)src;
|
||||
char *p1 = (char*)src, *p2 = (char*)target;
|
||||
char* p1_ahead = (char*)src;
|
||||
while (*++p2)
|
||||
p1_ahead++;
|
||||
while (*p1_ahead++)
|
||||
{
|
||||
char* p1_begin = p1;
|
||||
p2 = (char*)target;
|
||||
while (*p1 && *p2 && *p1++ == *p2++)
|
||||
{
|
||||
}
|
||||
if (!*p2) return p1_begin;
|
||||
p1 = p1_begin + 1;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 手写 memcpy()
|
||||
|
||||
```c++
|
||||
void* memcpy(void* dest, const void* src, size_t len)
|
||||
{
|
||||
char* d = (char *)dest;
|
||||
const char* s = (char *)src;
|
||||
while (len--)
|
||||
*d++ = *s++;
|
||||
return dest;
|
||||
}
|
||||
```
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [C++后台开发面试常见问题汇总](https://www.nowcoder.com/discuss/59394)
|
||||
- [函数strcpy、strcat和strcmp实现源码](http://blog.csdn.net/wangningyu/article/details/4662891)
|
||||
- [Github : gcc/libgcc/memcpy.c](https://github.com/gcc-mirror/gcc/blob/master/libgcc/memcpy.c)
|
||||
- [Leetcode : Implement strstr() to Find a Substring in a String](https://articles.leetcode.com/implement-strstr-to-find-substring-in/)
|
@ -1,164 +0,0 @@
|
||||

|
||||
|
||||
[TOC]
|
||||
|
||||
# 学习资料
|
||||
|
||||
> [git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
|
||||
|
||||
图文式简介
|
||||
|
||||
> [Github : my-git](https://github.com/xirong/my-git)
|
||||
|
||||
汇总式的资料收集和简介,下面的很多资料都是从这里找到的。
|
||||
|
||||
> [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
|
||||
|
||||
简洁明了。
|
||||
|
||||
> [廖雪峰 : Git教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
|
||||
|
||||
比较系统的对 git 进行介绍,很适合学习
|
||||
|
||||
> [Learn Git Branching](https://learngitbranching.js.org/)
|
||||
|
||||
交互式学习
|
||||
|
||||
# 集中式与分布式
|
||||
|
||||
Git 属于分布式版本控制系统,而 SVN 属于集中式。
|
||||
|
||||
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
|
||||
|
||||
集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。
|
||||
|
||||
集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件的会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。
|
||||
|
||||
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
|
||||
|
||||
# Git 的中心服务器
|
||||
|
||||
Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
|
||||
|
||||
# Git 工作流
|
||||
|
||||

|
||||
|
||||
新建一个版本库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
|
||||
|
||||
Git 版本库有一个称为 Stage 的暂存区,还有自动创建的 master 分支以及指向分支的 Head 指针。
|
||||
|
||||

|
||||
|
||||
|
||||
- git add files 把文件的修改添加到暂存区;
|
||||
- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了;
|
||||
- git reset -- files 用来撤销最后一次 git add files,也可以用 git reset 撤销所有暂存区。
|
||||
- git checkout -- files 把文件从暂存区复制到工作目录,用来丢弃本地修改。
|
||||
|
||||

|
||||
|
||||
可以跳过暂存区域直接从仓库取出文件或者直接提交代码。
|
||||
|
||||
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交。
|
||||
- git checkout HEAD -- files 回滚到复制最后一次提交。
|
||||
|
||||
# 分支实现
|
||||
|
||||
Git 把每次提交都连成一条时间线。
|
||||
|
||||
分支使用指针来实现,例如 master 分支指向时间线的最后一个节点,也就是最后一次提交。
|
||||
|
||||
HEAD 指针指向的是当前分支。
|
||||
|
||||

|
||||
|
||||
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
|
||||
|
||||

|
||||
|
||||
每次提交只会让当前分支向前移动,而其它分支不会移动。
|
||||
|
||||

|
||||
|
||||
合并分支也只需要改变指针即可。
|
||||
|
||||

|
||||
|
||||
# 冲突
|
||||
|
||||
当两个分支都对同一个文件进行了修改,在分支合并时就会产生冲突。
|
||||
|
||||

|
||||
|
||||
Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
Creating a new branch is quick & simple.
|
||||
=======
|
||||
Creating a new branch is quick AND simple.
|
||||
>>>>>>> feature1
|
||||
```
|
||||
|
||||
# Fast forward
|
||||
|
||||
"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
|
||||
|
||||
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
|
||||
|
||||
```
|
||||
$ git merge --no-ff -m "merge with no-ff" dev
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 分支管理策略
|
||||
|
||||
master 分支应该是非常稳定的,只用来发布新版本;
|
||||
|
||||
日常开发在开发分支 dev 上进行。
|
||||
|
||||

|
||||
|
||||
# 储藏(Stashing)
|
||||
|
||||
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
|
||||
|
||||
可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
|
||||
|
||||
```
|
||||
$ git stash
|
||||
Saved working directory and index state \ "WIP on master: 049d078 added the index file"
|
||||
HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
|
||||
```
|
||||
|
||||
该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
|
||||
|
||||
# SSH 传输设置
|
||||
|
||||
Git 仓库和 Github 中心仓库之间是通过 SSH 加密。
|
||||
|
||||
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
|
||||
|
||||
```
|
||||
$ ssh-keygen -t rsa -C "youremail@example.com"
|
||||
```
|
||||
|
||||
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
|
||||
|
||||
# .gitignore 文件
|
||||
|
||||
忽略以下文件:
|
||||
|
||||
1. 操作系统自动生成的文件,比如缩略图;
|
||||
2. 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
|
||||
3. 自己的敏感信息,比如存放口令的配置文件。
|
||||
|
||||
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
|
||||
|
||||
# Git 命令一览
|
||||
|
||||

|
||||
|
||||
比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
|
@ -1,143 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第 1 章 Github 课程介绍
|
||||
|
||||
Git 是一个版本控制工具,由 Linux 之父 Linus 编写完成。相比于 CVS 和 SVN 等集中式版本管理工具,Git 分布式的特点给它带来了很大的优势:首先它不需要像集中式的版本管理工具一样只有连网才能进行操作,其次它的分布式特点使得每个项目合作者本地都有一个项目的完整版本,这避免了集中式版本管理工具会出现的因为中央服务器故障而导致的代码丢失的问题。需要注意的是,Git 也有一个中央服务器,只不过这个中央服务器是用来分发各个合作者的版本的中介,不像集中式那样起到所有版本的管理者的角色。
|
||||
|
||||
Git 能够火起来有一个主要的原因是它体现了互联网的开源文化,GitHub 这个网站是基于 Git 而创建的,将这种开源文化发扬光大。GitHub 现在是全球最大的开源代码托管平台,它重新定义了软件开发的流程,因为很多软件功能都可以在 GitHub 上找到相应的源代码,而不需要重新编写,这大大减低了软件开发时间和成本。
|
||||
|
||||
Git 是一个命令行的工具,GitHub 的图形化界面让 Git 的使用更简单,可以结合 GitHub 的桌面客户端和 GitHub 的网站来一起使用。
|
||||
|
||||
# 第 2 章 浏览器中使用 Github
|
||||
|
||||
**commit** :GitHub 以一个 commit 作为一个版本。每当项目被修改后,需要进行一次 commit 操作。可以查看每个 commit 做的什么样的修改,包括修改时间,修改人,修改内容。一个 commit 在 GitHub 中 具有一个版本号,还有父版本号,父版本号是为了记录版本的历史线。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 第 3 章 GitHub 客户端的使用
|
||||
|
||||
本地有更好用的编辑器和 IDE ,因此基本上需要在本地使用 GitHub。使用桌面客户端可以更简单方便地使用 GitHub,如果直接使用 Git 命令行工具,会暴露在复杂的概念面前,不容易掌握。
|
||||
|
||||
在本地的仓库里面进行修改后,会在桌面客户端自动显示 change。
|
||||
|
||||

|
||||
|
||||
一个 commit 需要有 Summary 和 Description,以便以后回头看这个 commit 能够知道进行了哪些修改。
|
||||
|
||||
Publish 用于发布一个本地的 repostory 到 GitHub 上。发布之后,可以用 Sync 同步本地的 commit 到 GitHub 服务器上。
|
||||
|
||||
客户端上的 Undo 只能是撤销还未同步的一个版本,而 Revert 才能撤销已经同步的版本,它本身也作为一个 commit 提交到服务器,抵消了上一次的 commit 做的修改。
|
||||
|
||||
Roll Back to this Commit 可以回滚到选定的一个版本,也是作为一个 commit 提交到服务器进行操作。
|
||||
|
||||
# 第 4 章 简单分支操作
|
||||
|
||||
分支是用指针来实现的,master 分支的指针指向最新的一个版本。
|
||||
|
||||

|
||||
|
||||
新建一个分支,没有复制各个版本,而是新建的一个指针和 master 指向同一个版本,在这个分支上的操作只是移动这个分支的指针。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
新建分支的目的是为了完成一个新想法,而不去污染 master 分支。
|
||||
|
||||
可以自由的切换分支,在不同分支上进行操作互不影响。
|
||||
|
||||
要发布一个新分支发布到 GitHub 上也是用 Publish。
|
||||
|
||||
删除一个分支:当前分支不可删除,需要切换到其它分支才可以删除。Unpublish 是不发布分支,也就是删除远程服务器上的分支,但是本地上的分支会保留;而 Delete 是直接删除本地分支,并且远程服务器上的分支也会被删除。
|
||||
|
||||
master 是默认分支,不能被删除,可以通过在设置中修改默认分支为其它分支然后再删除 master 分支。需要到 Github 网站上的 setting 中设置默认分支。
|
||||
|
||||
# 第 5 章 分支合并
|
||||
|
||||
通过 merge 合并分支之后,会以一个 commit 的形式提交,称为融合版本,它有两个父版本号,并且 master 指针会指向这个 commit。
|
||||
|
||||

|
||||
|
||||
在并行开发过程中,两个分支可能同时进行修改,如果对同一个文件进行了而修改,就会造成冲突。
|
||||
|
||||
本地 master 和远程 master 可以看成两个分支,同样也可能出现冲突的情况,使用 Sync 就是一个合并本地 master 分支和远端 master 分支的操作。
|
||||
|
||||
发生冲突的文件会自动添加冲突标识符,修改的时候也要把冲突标示符删除。
|
||||
|
||||

|
||||
|
||||
# 第 6 章 团队协作流程
|
||||
|
||||
通过 Github 网站的 Setting 中的 Collaborators 可以为一个仓库添加协作者,协作者对项目具有写权限。
|
||||
|
||||
每个协作者可以直接对仓库进行修改,但是很容易产生冲突,只有在做一点小修改时才这么做。当开发新功能时,一般是新建一个新分支,然后在新分支上进行修改。新分支要合并到 master 分支上之前,先提交一个 pull request 来引发讨论和代码审核,之后再进行合并操作。
|
||||
|
||||
新建分支之后,使用 Sync 就可以发布一个 pull request,而不是用 publish 该分支。
|
||||
|
||||
**Github Flow**
|
||||
|
||||
1. 新建话题分支
|
||||

|
||||
|
||||
2. 实现功能,做成一个个新版本
|
||||

|
||||
|
||||
3. 发起 pull request,就是请求项目参与者 pull 该分支并进行审查和讨论。
|
||||

|
||||
|
||||
4. 审查和讨论。该过程的讨论和论坛上类似,而且该过程也可以继续提交新的 commit,而不用再次发 pull request。
|
||||

|
||||

|
||||
|
||||
5. 最后协作者通过 merge pull request 来合并
|
||||

|
||||
|
||||
# 第 7 章 开源项目共享流程
|
||||
|
||||

|
||||
|
||||
开源项目以陌生人为主,不可能每个人都成为 Collaborators。
|
||||
|
||||
fork 之后相当于拷贝一份仓库到自己的 Github 上,类似于新建了一个分支。
|
||||
|
||||
如果只是对某个文件进行修改,那么可以在 Github 网站上通过修改按钮进行修改,会自动创建一个 pull request,而不用先 fork。
|
||||
|
||||
|
||||
# 第 8 章 GitHub Issues
|
||||
|
||||
Github 提供了三种辅助设施:GitHub Pages:搭建项目主页、Wiki:知识库、Issues:事务卡片。
|
||||
|
||||
一个 Issues 相当于一个讨论区,可以 @ 某个人让他参与进来。每个 Issues 都有编号,用 #+ 编号 可以指定某个 Issues。当要回复前面某个评论时,可以用 > 引用前面的内容,然后再在下方写回复。> 是 markdown 的一个标记,其后面需要跟上引用内容。一个评论也有特定的链接,可以在新评论中插入该链接来快速定位。
|
||||
|
||||

|
||||
|
||||
Pull Request 会创建一个 Issues。
|
||||
|
||||
在一个 commit 的 sumnary 内容后面加上 Issues 号,会自动在 Issues 后面新增这个 commit 内容。
|
||||
|
||||

|
||||
|
||||
# 第 9 章 GitHub Pages
|
||||
|
||||
可以搭建的网站分为用户和组织、项目。
|
||||
|
||||
在项目中新建一个 gh-pages 分支,可以删除里面的其它文件,只放网页的文件,比如 index.html。域名为 用户名 .Github.io/ 项目名。
|
||||
|
||||
可以使用 Jekyll 等前端框架。
|
||||
|
||||
# 第 10 章 GitHub 的秘密机关
|
||||
|
||||
在 Github 网站的项目主页上按 t 快捷键可以打开搜索项目文件的界面。
|
||||
|
||||

|
||||
|
||||
# 第 11 章 Until Next Time,Goodbye
|
||||
|
||||
GitLab 是一个开源免费的类似于 GitHub 的项目,可以用它来搭建私有仓库。
|
||||
|
||||
# 资源
|
||||
|
||||
- [ 课程地址 ](http://www.imooc.com/learn/390)
|
||||
- [ 课程的文档地址 ](http://book.haoduoshipin.com/Gitbeijing/)
|
@ -1,29 +0,0 @@
|
||||
# 各个 HTTP 版本的对比
|
||||
|
||||
1997 年 1 月公布的 HTTP/1.1 是目前主流的 HTTP 协议版本。HTTP/2 标准于 2015 年 5 月以 RFC 7540 正式发表。HTTP/2 的标准化工作由Chrome、Opera、Firefox、Internet Explorer 11、Safari、Amazon Silk 及 Edge 等浏览器提供支持。多数主流浏览器已经在 2015 年底支持了该协议。此外,根据 W3Techs 的数据,在 2017 年 5 月,在排名前一千万的网站中,有 13.7% 支持了 HTTP/2。
|
||||
|
||||
## HTTP/1.0 与 HTTP/1.1 的区别
|
||||
|
||||
- HTTP/1.1 默认是长连接;
|
||||
- HTTP/1.1 提供了范围请求功能;
|
||||
- HTTP/1.1 提供了虚拟主机的功能;
|
||||
- HTTP/1.1 多了一些缓存处理字段;
|
||||
- HTTP/1.1 多了一些状态码;
|
||||
|
||||
## HTTP/1.1 与 HTTP/2.0 的区别
|
||||
|
||||
**多路复用**
|
||||
|
||||
HTTP/2.0 使用多路复用技术,即使用同一个 TCP 连接来处理多个请求。
|
||||
|
||||
**首部压缩**
|
||||
|
||||
HTTP1.1 的首部带有大量信息,而且每次都要重复发送,HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。
|
||||
|
||||
**服务端推送**
|
||||
|
||||
在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。
|
||||
|
||||
**二进制格式**
|
||||
|
||||
HTTP1.1 的解析是基于文本的,而 HTTP2.0 采用二进制格式。
|
@ -1,360 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 基础概念
|
||||
|
||||
## Web基础
|
||||
|
||||
HTTP(HyperText Transfer Protocol,超为本传输协议)。
|
||||
|
||||
WWW(Word Wide Web)的三种技术:HTML、HTTP、URL。
|
||||
|
||||
RFC(Request for Comments,征求修正意见书),互联网的设计文档。
|
||||
|
||||
## URL
|
||||
|
||||
URI(Uniform Resource Indentifier,统一资源标识符),URL(Uniform Resource Locator,统一资源定位符),URN(Uniform Resource Name,统一资源名称),例如 urn:isbn:0-486-27557-4 。URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
|
||||
|
||||
URL格式:
|
||||
|
||||

|
||||
|
||||
## 请求和响应报文
|
||||
|
||||
**请求报文**
|
||||
|
||||

|
||||
|
||||
**响应报文**
|
||||
|
||||

|
||||
|
||||
# HTTP 方法
|
||||
|
||||
客户端发送的请求报文第一行为请求行,包含了方法字段。
|
||||
|
||||
## GET:获取资源
|
||||
|
||||
## POST:传输实体主体
|
||||
|
||||
POST 主要目的不是获取资源,而是传输实体主体数据。
|
||||
|
||||
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL中,而 POST 的参数存储在实体主体部分。
|
||||
|
||||
```
|
||||
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
|
||||
```
|
||||
```
|
||||
POST /test/demo_form.asp HTTP/1.1
|
||||
Host: w3schools.com
|
||||
name1=value1&name2=value2
|
||||
```
|
||||
|
||||
GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。
|
||||
|
||||
## HEAD:获取报文首部
|
||||
|
||||
和 GET 方法一样,但是不返回报文实体主体部分。
|
||||
|
||||
主要用于确认 URL 的有效性以及资源更新的日期时间等。
|
||||
|
||||
## PUT:上传文件
|
||||
|
||||
由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般 WEB 网站不使用该方法。
|
||||
|
||||
## DELETE:删除文件
|
||||
|
||||
与 PUT 功能相反,并且同样不带验证机制。
|
||||
|
||||
## OPTIONS:查询支持的方法
|
||||
|
||||
查询指定的 URL 能够支持的方法。
|
||||
|
||||
会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。
|
||||
|
||||
## RACE:追踪路径
|
||||
|
||||
服务器会将通信路径返回给客户端。
|
||||
|
||||
发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
|
||||
|
||||
TRACE 一般不会使用,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪),因此更不会去使用它。
|
||||
|
||||

|
||||
|
||||
## CONNECT:要求用隧道协议连接代理
|
||||
|
||||
用隧道协议进行 TCP 通信。
|
||||
|
||||
主要使用 SSL(Secure Sokets Layer,安全套接字)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
|
||||
|
||||

|
||||
|
||||
# HTTP 状态码
|
||||
|
||||
服务器返回的响应报文中第一行为状态行,包含了状态码以及原因短语,来告知客户端请求的结果。
|
||||
|
||||
| 状态码 | 类别 | 原因短语 |
|
||||
| --- | --- | --- |
|
||||
| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
|
||||
| 2XX | Success(成功状态码) | 请求正常处理完毕 |
|
||||
| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
|
||||
| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
|
||||
| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
|
||||
|
||||
## 2XX 成功
|
||||
|
||||
**200 OK**
|
||||
|
||||
**204 No Content**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
|
||||
|
||||
**206 Partial Content**
|
||||
|
||||
## 3XX 重定向
|
||||
|
||||
**301 Moved Permanently**:永久性重定向
|
||||
|
||||
**302 Found**:临时性重定向
|
||||
|
||||
**303 See Other**
|
||||
|
||||
注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会把 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
|
||||
|
||||
**304 Not Modified**:如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,但是不满足条件,则服务器会返回 304 状态码。
|
||||
|
||||
**307 Temporary Redirect**:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
|
||||
|
||||
## 4XX 客户端错误
|
||||
|
||||
**400 Bad Request**:请求报文中存在语法错误
|
||||
|
||||
**401 Unauthorized**:该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。如果之前已进行过一次请求,则表示用户认证失败。
|
||||
|
||||

|
||||
|
||||
**403 Forbidden**:请求被拒绝,服务器端没有必要给出拒绝的详细理由。
|
||||
|
||||
**404 Not Found**
|
||||
|
||||
## 5XX 服务器错误
|
||||
|
||||
**500 Internal Server Error**:服务器正在执行请求时发生错误
|
||||
|
||||
**503 Service Unavilable**:该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
|
||||
|
||||
# HTTP首部
|
||||
|
||||
HTTP 报文包含了首部和主体两部分。有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。各种首部字段及其含义如下(不需要全记,仅供查阅):
|
||||
|
||||
## 通用首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| -- | -- |
|
||||
| Cache-Control | 控制缓存的行为 |
|
||||
| Connection | 逐跳首部、 连接的管理 |
|
||||
| Date | 创建报文的日期时间 |
|
||||
| Pragma | 报文指令 |
|
||||
| Trailer | 报文末端的首部一览 |
|
||||
| Transfer-Encoding | 指定报文主体的传输编码方式 |
|
||||
| Upgrade | 升级为其他协议 |
|
||||
| Via | 代理服务器的相关信息 |
|
||||
| Warning | 错误通知 |
|
||||
|
||||
## 请求首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| -- | -- |
|
||||
| Accept | 用户代理可处理的媒体类型 |
|
||||
| Accept-Charset | 优先的字符集 |
|
||||
| Accept-Encoding | 优先的内容编码 |
|
||||
| Accept-Language | 优先的语言(自然语言) |
|
||||
| Authorization | Web认证信息 |
|
||||
| Expect | 期待服务器的特定行为 |
|
||||
| From | 用户的电子邮箱地址 |
|
||||
| Host | 请求资源所在服务器 |
|
||||
| If-Match | 比较实体标记(ETag) |
|
||||
| If-Modified-Since | 比较资源的更新时间 |
|
||||
| If-None-Match | 比较实体标记(与 If-Match 相反) |
|
||||
| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
|
||||
| If-Unmodified-Since | 比较资源的更新时间(与If-Modified-Since相反) |
|
||||
| Max-Forwards | 最大传输逐跳数 |
|
||||
| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
|
||||
| Range | 实体的字节范围请求 |
|
||||
| Referer | 对请求中 URI 的原始获取方 |
|
||||
| TE | 传输编码的优先级 |
|
||||
| User-Agent | HTTP 客户端程序的信息 |
|
||||
|
||||
## 响应首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| -- | -- |
|
||||
| Accept-Ranges | 是否接受字节范围请求 |
|
||||
| Age | 推算资源创建经过时间 |
|
||||
| ETag | 资源的匹配信息 |
|
||||
| Location | 令客户端重定向至指定URI |
|
||||
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
|
||||
| Retry-After | 对再次发起请求的时机要求 |
|
||||
| Server | HTTP服务器的安装信息 |
|
||||
| Vary | 代理服务器缓存的管理信息 |
|
||||
| WWW-Authenticate | 服务器对客户端的认证信息 |
|
||||
|
||||
## 实体首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| -- | -- |
|
||||
| Allow | 资源可支持的HTTP方法 |
|
||||
| Content-Encoding | 实体主体适用的编码方式 |
|
||||
| Content-Language | 实体主体的自然语言 |
|
||||
| Content-Length | 实体主体的大小(单位: 字节) |
|
||||
| Content-Location | 替代对应资源的URI |
|
||||
| Content-MD5 | 实体主体的报文摘要 |
|
||||
| Content-Range | 实体主体的位置范围 |
|
||||
| Content-Type | 实体主体的媒体类型 |
|
||||
| Expires | 实体主体过期的日期时间 |
|
||||
| Last-Modified | 资源的最后修改日期时间 |
|
||||
|
||||
# 具体应用
|
||||
|
||||
## Cookie
|
||||
|
||||
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
|
||||
|
||||
服务器会发送的响应报文包含 Set-Cookie 字段,客户端得到该相应后把 Cookie 内容保存到浏览器中。下次再发送请求时,从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段,这样服务器就知道客户端的状态信息了。Cookie 状态信息保存在客户端浏览器中,而不是服务器上。
|
||||
|
||||

|
||||
|
||||
Set-Cookie 字段有以下属性:
|
||||
|
||||
| 属性 | 说明 |
|
||||
| -- | -- |
|
||||
| NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
|
||||
| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
|
||||
| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) |
|
||||
| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) |
|
||||
| Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
|
||||
| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
|
||||
|
||||
**Session 和 Cookie 区别**
|
||||
|
||||
Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文就包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,而 Session 用于服务器端,Cookie 用于客户端。
|
||||
|
||||
**浏览器禁用 Cookie 的情况**
|
||||
|
||||
会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。
|
||||
|
||||
**使用 Cookie 实现用户名和密码的自动填写**
|
||||
|
||||
网站脚本会自动从 Cookie 中读取用户名和密码,从而实现自动填写。
|
||||
|
||||
## 缓存
|
||||
|
||||
有两种缓存方法:让代理服务器进行缓存和让客户端浏览器进行缓存。
|
||||
|
||||
Cache-Control 用于控制缓存的行为。
|
||||
|
||||
Cache-Control: no-cache 有两种含义,如果是客户端向缓存服务器发送的请求报文中含有该指令,表示客户端不想要缓存的资源;如果是源服务器向缓存服务器发送的响应报文中含有该指令,表示缓存服务器不能对资源进行缓存。
|
||||
|
||||
Expires 字段可以用于告知缓存服务器该资源什么时候会过期。当首部字段 Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。
|
||||
|
||||
## 持久连接
|
||||
|
||||
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。**持久连接** 只需要进行一次 TCP 连接就能进行多次 HTTP 通信。HTTP/1.1开始,所有的连接默认都是持久连接。
|
||||
|
||||

|
||||
|
||||
持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection: close ;而在HTTP/1.1之前默认是非持久化连接的,如果要维持持续连接,需要使用 Keep-Alive。
|
||||
|
||||
管线化方式可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
|
||||
|
||||

|
||||
|
||||
## 编码
|
||||
|
||||
编码(Encoding)主要是为了对实体进行压缩。常用的编码有:gzip、compress、deflate、identity,其中 identity 表示不执行压缩的编码格式。
|
||||
|
||||
## 分块传输
|
||||
|
||||
分块传输(Chunked Transfer Coding)可以把数据分割成多块,让浏览器逐步显示页面。
|
||||
|
||||
## 多部分对象集合
|
||||
|
||||
一份报文主体内可含有多类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔;每个部分都可以有首部字段。
|
||||
|
||||
例如,上传多个表单时可以使用如下方式:
|
||||
|
||||

|
||||
|
||||
## 范围请求
|
||||
|
||||
如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。
|
||||
|
||||
在请求报文首部中添加 Range 字段,然后指定请求的范围,例如 Range : bytes = 5001-10000。请求成功的话服务器发送 206 Partial Content 状态。
|
||||
|
||||
## 内容协商
|
||||
|
||||
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
|
||||
|
||||
涉及以下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。
|
||||
|
||||
## 虚拟主机
|
||||
|
||||
使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
|
||||
|
||||
## 通信数据转发
|
||||
|
||||
**代理**
|
||||
|
||||
代理服务器接受客户端的请求,并且转发给其它服务器。代理服务器一般是透明的,不会改变 URL。
|
||||
|
||||
使用代理的主要目的是:缓存、网络访问控制以及记录访问日志。
|
||||
|
||||

|
||||
|
||||
**网关**
|
||||
|
||||
与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而其它非 HTTP 服务器的服务。
|
||||
|
||||

|
||||
|
||||
**隧道**
|
||||
|
||||
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
|
||||
|
||||

|
||||
|
||||
# HTTPs
|
||||
|
||||
HTTP 有以下安全性问题:
|
||||
|
||||
1. 通信使用明文,内容可能会被窃听;
|
||||
2. 不验证通信方的身份,因此有可能遭遇伪装;
|
||||
3. 无法证明报文的完整性,所以有可能已遭篡改。
|
||||
|
||||
HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Socket Layer)通信,再由 SSL 和 TCP 通信。通过使用 SSL,HTTPs 提供了加密、认证和完整性保护。
|
||||
|
||||
## 加密
|
||||
|
||||
有两种加密方式:对称密钥加密和公开密钥加密。对称密钥加密的加密和解密使用同一密钥,而公开密钥加密使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
|
||||
|
||||
对称密钥加密的缺点:无法安全传输密钥;公开密钥加密的缺点:相对来说更耗时。
|
||||
|
||||
HTTPs 采用 **混合的加密机制**,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中,共享密钥即对称密钥)
|
||||
|
||||

|
||||
|
||||
## 认证
|
||||
|
||||
通过使用 **证书** 来对通信方进行认证。证书中有公开密钥数据,如果可以验证公开密钥的确属于通信方的,那么就可以确定通信方是可靠的。
|
||||
|
||||
数字证书认证机构(CA,Certificate
|
||||
Authority)颁发的公开密钥证书,可以通过 CA 对其进行验证。
|
||||
|
||||
进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,就可以开始加密过程。
|
||||
|
||||
使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。
|
||||
|
||||
客户端证书需要用户自行安装,只有在业务需要非常高安全性时才使用客户端证书,例如网上银行。
|
||||
|
||||
## 完整性
|
||||
|
||||
SSL 提供摘要功能来验证完整性。
|
||||
|
@ -1,611 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 内存模型
|
||||
|
||||

|
||||
|
||||
注:白色区域为线程私有的,蓝色区域为线程共享的。
|
||||
|
||||
## 1. 程序计数器
|
||||
|
||||
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是 Native 方法则为空)。
|
||||
|
||||
## 2. Java 虚拟机栈
|
||||
|
||||
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
||||
|
||||
该区域可能抛出以下异常:
|
||||
|
||||
1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
|
||||
2. 栈进行动态扩展时如果无法申请导足够内存,会抛出 OutOfMemoryError 异常。
|
||||
|
||||
## 3. 本地方法栈
|
||||
|
||||
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
## 4. Java 堆
|
||||
|
||||
所有对象实例都在这里分配内存。
|
||||
|
||||
这块区域是垃圾收集器管理的主要区域("GC 堆 ")。现在收集器基本都是采用分代收集算法,Java 堆还可以分成:新生代和老年代(新生代还可以分成 Eden 空间、From Survivor 空间、To Survivor 空间等)。
|
||||
|
||||
不需要连续内存,可以通过 -Xmx 和 -Xms 来控制动态扩展内存大小,如果动态扩展失败会抛出 OutOfMemoryError 异常。
|
||||
|
||||
## 5. 方法区
|
||||
|
||||
用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||
|
||||
和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
|
||||
|
||||
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。
|
||||
|
||||
## 6. 运行时常量池
|
||||
|
||||
运行时常量池是方法区的一部分。
|
||||
|
||||
类加载后,Class 文件中的常量池(用于存放编译期生成的各种字面量和符号引用)就会被放到这个区域。
|
||||
|
||||
在运行期间也可以用过 String 类的 intern() 方法将新的常量放入该区域。
|
||||
|
||||
## 7. 直接内存
|
||||
|
||||
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
|
||||
|
||||
# 垃圾收集
|
||||
|
||||
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
|
||||
|
||||
垃圾回收主要是针对 Java 堆和方法区进行。
|
||||
|
||||
## 1. 判断一个对象是否可回收
|
||||
|
||||
### 1.1 引用计数
|
||||
|
||||
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。
|
||||
|
||||
引用计数为 0 的对象可被回收。
|
||||
|
||||
两个对象会出现循环引用问题,此时引用计数器永远不为 0,导致 GC 收集器无法回收。
|
||||
|
||||
```java
|
||||
objA.instance = objB;
|
||||
objB.instance = objA;
|
||||
```
|
||||
|
||||
### 1.2 可达性
|
||||
|
||||
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收。
|
||||
|
||||
GC Roots 一般包含以下内容:
|
||||
|
||||
1. 虚拟机栈中引用的对象
|
||||
2. 方法区中类静态属性引用的对象
|
||||
3. 方法区中的常量引用的对象
|
||||
4. 本地方法栈中引用的对象
|
||||
|
||||
### 1.3 引用类型
|
||||
|
||||
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定独享是否存活都与“引用”有关。
|
||||
|
||||
#### 1.3.1 强引用
|
||||
|
||||
只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
```
|
||||
|
||||
#### 1.3.2 软引用
|
||||
|
||||
|
||||
非必须引用,内存溢出之前进行回收。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
SoftReference<Object> sf = new SoftReference<Object>(obj);
|
||||
obj = null;
|
||||
sf.get();
|
||||
```
|
||||
|
||||
sf 是对 obj 的一个软引用,通过 sf.get() 方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回 null;
|
||||
|
||||
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
|
||||
|
||||
|
||||
#### 1.3.3 弱引用
|
||||
|
||||
只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
WeakReference<Object> wf = new WeakReference<Object>(obj);
|
||||
obj = null;
|
||||
wf.get();
|
||||
wf.isEnQueued();
|
||||
```
|
||||
|
||||
#### 1.3.4 虚引用
|
||||
|
||||
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
|
||||
obj=null;
|
||||
pf.get();
|
||||
pf.isEnQueued();
|
||||
```
|
||||
|
||||
### 1.3 方法区的回收
|
||||
|
||||
在方法区主要是对常量池的回收和对类的卸载。
|
||||
|
||||
常量池的回收和堆中对象回收类似。
|
||||
|
||||
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
|
||||
|
||||
1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
|
||||
2. 加载该类的 ClassLoader 已经被回收。
|
||||
3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
|
||||
|
||||
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
|
||||
|
||||
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
|
||||
|
||||
### 1.4 finalize()
|
||||
|
||||
当一个对象可被回收时,如果该对象有必要执行 finalize() 方法,那么就有可能可能通过在该方法中让对象重新被引用,从而实现自救。
|
||||
|
||||
finalize() 类似 C++ 的虚构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
|
||||
|
||||
## 2. 垃圾收集算法
|
||||
|
||||
### 2.1 标记 - 清除算法
|
||||
|
||||

|
||||
|
||||
将需要回收的对象进行标记,然后清除。
|
||||
|
||||
不足:
|
||||
|
||||
1. 标记和清除过程效率都不高
|
||||
2. 会产生大量碎片
|
||||
|
||||
之后的算法都是基于该算法进行改进。
|
||||
|
||||
### 2.2 复制算法
|
||||
|
||||

|
||||
|
||||
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
|
||||
|
||||
主要不足是只使用了内存的一半。
|
||||
|
||||
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,需要依赖于老年代进行分配担保,也就是借用老年代的空间。
|
||||
|
||||
### 2.3 标记 - 整理算法
|
||||
|
||||

|
||||
|
||||
让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。
|
||||
|
||||
### 2.4 分代收集算法
|
||||
|
||||
现在的商业虚拟机采用分代收集算法,它使用了前面介绍的几种收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
|
||||
|
||||
一般将 Java 堆分为新生代和老年代。
|
||||
|
||||
1. 新生代使用:复制算法
|
||||
2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法。
|
||||
|
||||
## 3. 垃圾收集器
|
||||
|
||||

|
||||
|
||||
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
|
||||
|
||||
### 3.1 Serial 收集器
|
||||
|
||||

|
||||
|
||||
它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。
|
||||
|
||||
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
|
||||
|
||||
在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
|
||||
|
||||
### 3.2 ParNew 收集器
|
||||
|
||||

|
||||
|
||||
它是 Serial 收集器的多线程版本。
|
||||
|
||||
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
|
||||
|
||||
默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
|
||||
|
||||
### 3.3 Parallel Scavenge 收集器
|
||||
|
||||
是并行的多线程收集器。
|
||||
|
||||
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
|
||||
|
||||
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
|
||||
|
||||
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
|
||||
|
||||
还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。
|
||||
|
||||
### 3.4 Serial Old 收集器
|
||||
|
||||

|
||||
|
||||
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
|
||||
|
||||
1. 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
|
||||
2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
|
||||
|
||||
### 3.5 Parallel Old 收集器
|
||||
|
||||

|
||||
|
||||
是 Parallel Scavenge 收集器的老年代版本。
|
||||
|
||||
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
|
||||
|
||||
### 3.6 CMS 收集器
|
||||
|
||||

|
||||
|
||||
CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于 标记 - 清除 算法实现的。
|
||||
|
||||
特点:并发收集、低停顿。
|
||||
|
||||
分为以下四个流程:
|
||||
|
||||
1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
|
||||
2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
|
||||
3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
|
||||
4. 并发清除:不需要停顿。
|
||||
|
||||
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
|
||||
|
||||
具有以下缺点:
|
||||
|
||||
1. 对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。
|
||||
|
||||
2. 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。
|
||||
|
||||
3. 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前出发一次 Full GC。
|
||||
|
||||
### 3.7 G1 收集器
|
||||
|
||||

|
||||
|
||||
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。
|
||||
|
||||
具备如下特点:
|
||||
|
||||
- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间;
|
||||
- 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。
|
||||
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
|
||||
- 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。
|
||||
|
||||
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样,Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合。
|
||||
|
||||
之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。
|
||||
|
||||
Region 不可能是孤立的,一个对象分配在某个 Region 中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。
|
||||
|
||||
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
|
||||
|
||||
1. 初始标记
|
||||
2. 并发标记
|
||||
3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
|
||||
4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
|
||||
|
||||
### 3.8 七种垃圾收集器的比较
|
||||
|
||||
| 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
|
||||
| **Serial Old** | 串行 | 老年代 | 标记 - 整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
|
||||
| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
|
||||
| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
|
||||
| **Parallel Old** | 并行 | 老年代 | 标记 - 整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
|
||||
| **CMS** | 并发 | 老年代 | 标记 - 清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
|
||||
| **G1** | 并发 | both | 标记 - 整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
|
||||
|
||||
## 4. 内存分配与回收策略
|
||||
|
||||
### 4.1 优先在 Eden 分配
|
||||
|
||||
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC;
|
||||
|
||||
### 4.2 大对象直接进入老年代
|
||||
|
||||
提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制;
|
||||
### 4.3 长期存活的对象进入老年代
|
||||
|
||||
JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor 区容纳的,移动到 Survivor 区,年龄加 1,每经历一次 Minor GC 不被清理则年龄加 1,增加到一定年龄则移动到老年区(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置);
|
||||
|
||||
|
||||
### 4.4 动态对象年龄判定
|
||||
|
||||
若 Survivor 区中同年龄所有对象大小总和大于 Survivor 空间一半,则年龄大于等于该年龄的对象可以直接进入老年代;
|
||||
|
||||
### 4.5 空间分配担保
|
||||
|
||||
在发生 Minor GC 之前,JVM 先检查老年代最大可用连续空间是否大于新生代所有对象总空间,成立的话 Minor GC 确认是安全的;否则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大于的话进行 Minor GC,小于的话进行 Full GC。
|
||||
|
||||
## 4.6 Full GC 的触发条件
|
||||
|
||||
对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
|
||||
|
||||
### 4.6.1 调用 System.gc()
|
||||
|
||||
此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存,可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。
|
||||
|
||||
### 4.6.2 老年代空间不足
|
||||
|
||||
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出如下错误: Java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
|
||||
|
||||
### 4.6.3 空间分配担保失败
|
||||
|
||||
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
|
||||
|
||||
### 4.6.4 JDK 1.7 及以前的永久代空间不足
|
||||
|
||||
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space 为避免 PermGen 占满造成 Full GC 现象,可采用的方法为增大 PermGen 空间或转为使用 CMS GC。
|
||||
|
||||
在 JDK 1.8 中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。
|
||||
|
||||
### 4.6.5 Concurrent Mode Failure
|
||||
|
||||
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
|
||||
|
||||
# 类加载机制
|
||||
|
||||
类是在运行期间动态加载的。
|
||||
|
||||
## 1 类的生命周期
|
||||
|
||||

|
||||
|
||||
包括以下 7 个阶段:
|
||||
|
||||
- **加载(Loading)**
|
||||
- **验证(Verification)**
|
||||
- **准备(Preparation)**
|
||||
- **解析(Resolution)**
|
||||
- **初始化(Initialization)**
|
||||
- 使用(Using)
|
||||
- 卸载(Unloading)
|
||||
|
||||
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
|
||||
|
||||
## 2. 类初始化时机
|
||||
|
||||
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化:( 加载、验证、准备都会随着发生 )
|
||||
|
||||
1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
|
||||
|
||||
2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
|
||||
|
||||
3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
|
||||
|
||||
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
|
||||
|
||||
5. 当使用 jdk1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
|
||||
|
||||
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
|
||||
|
||||
1\. 通过子类引用父类的静态字段,不会导致子类初始化。
|
||||
|
||||
```java
|
||||
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
|
||||
```
|
||||
|
||||
2\. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
|
||||
|
||||
```java
|
||||
SuperClass[] sca = new SuperClass[10];
|
||||
```
|
||||
|
||||
3\. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
|
||||
|
||||
```java
|
||||
System.out.println(ConstClass.HELLOWORLD);
|
||||
```
|
||||
|
||||
## 3. 类加载过程
|
||||
|
||||
包含了加载、验证、准备、解析和初始化这 5 个阶段。
|
||||
|
||||
### 3.1 加载
|
||||
|
||||
加载是类加载的一个阶段,注意不要混淆。
|
||||
|
||||
加载过程完成以下三件事:
|
||||
|
||||
1. 通过一个类的全限定名来获取定义此类的二进制字节流。
|
||||
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
|
||||
3. 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
|
||||
|
||||
其中二进制字节流可以从以下方式中获取:
|
||||
|
||||
- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
|
||||
- 从网络中获取,这种场景最典型的应用是 Applet。
|
||||
- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
|
||||
- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
|
||||
- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
|
||||
...
|
||||
|
||||
### 3.2 验证
|
||||
|
||||
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
|
||||
|
||||
主要有以下 4 个阶段:
|
||||
|
||||
1. 文件格式验证
|
||||
2. 元数据验证(对字节码描述的信息进行语义分析)
|
||||
3. 字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析)
|
||||
4. 符号引用验证
|
||||
|
||||
### 3.3 准备
|
||||
|
||||
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
|
||||
|
||||
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。
|
||||
|
||||
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
|
||||
|
||||
```java
|
||||
public static int value = 123;
|
||||
```
|
||||
|
||||
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
|
||||
|
||||
```java
|
||||
public static final int value = 123;
|
||||
```
|
||||
|
||||
### 3.4 解析
|
||||
|
||||
将常量池的符号引用替换为直接引用的过程。
|
||||
|
||||
### 3.5 初始化
|
||||
|
||||
初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
|
||||
|
||||
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit>() 方法具有以下特点:
|
||||
|
||||
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
|
||||
|
||||
- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:
|
||||
|
||||
```java
|
||||
static class Parent {
|
||||
public static int A = 1;
|
||||
static {
|
||||
A = 2;
|
||||
}
|
||||
}
|
||||
|
||||
static class Sub extends Parent {
|
||||
public static int B = A;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(Sub.B); // 输出结果是父类中的静态变量值 A,也就是 2
|
||||
}
|
||||
```
|
||||
|
||||
- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
|
||||
|
||||
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
|
||||
|
||||
- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
## 4. 类加载器
|
||||
|
||||
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
|
||||
|
||||
### 4.1 类与类加载器
|
||||
|
||||
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字对做对象所属关系判定等情况),只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
|
||||
|
||||
### 4.2 类加载器分类
|
||||
|
||||
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
|
||||
|
||||
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;另一种就是所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
|
||||
|
||||
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
|
||||
|
||||
- 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用 null 代替即可。
|
||||
|
||||
- 扩展类加载器(Extension ClassLoader) 这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <Java_Home>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
|
||||
|
||||
- 应用程序类加载器(Application ClassLoader) 这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
|
||||
|
||||
### 4.3 双亲委派模型
|
||||
|
||||
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
|
||||
|
||||

|
||||
|
||||
**工作过程**
|
||||
|
||||
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
|
||||
|
||||
**好处**
|
||||
|
||||
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放再 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为`java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
|
||||
|
||||
**实现**
|
||||
|
||||
```java
|
||||
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
|
||||
//check the class has been loaded or not
|
||||
Class c = findLoadedClass(name);
|
||||
if(c == null) {
|
||||
try{
|
||||
if(parent != null) {
|
||||
c = parent.loadClass(name, false);
|
||||
} else{
|
||||
c = findBootstrapClassOrNull(name);
|
||||
}
|
||||
} catch(ClassNotFoundException e) {
|
||||
//if throws the exception , the father can not complete the load
|
||||
}
|
||||
if(c == null) {
|
||||
c = findClass(name);
|
||||
}
|
||||
}
|
||||
if(resolve) {
|
||||
resolveClass(c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
```
|
||||
|
||||
# JVM 参数
|
||||
|
||||
## GC 优化配置
|
||||
|
||||
| 配置 | 描述 |
|
||||
| --- | --- |
|
||||
| -Xms | 初始化堆内存大小 |
|
||||
| -Xmx | 堆内存最大值 |
|
||||
| -Xmn | 新生代大小 |
|
||||
| -XX:PermSize | 初始化永久代大小 |
|
||||
| -XX:MaxPermSize | 永久代最大容量 |
|
||||
|
||||
## GC 类型设置
|
||||
|
||||
| 配置 | 描述 |
|
||||
| --- | --- |
|
||||
| -XX:+UseSerialGC | 串行垃圾回收器 |
|
||||
| -XX:+UseParallelGC | 并行垃圾回收器 |
|
||||
| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
|
||||
| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
|
||||
| -XX:+UseG1GC | G1 垃圾回收器 |
|
||||
|
||||
```java
|
||||
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
|
||||
```
|
@ -1,376 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 概览
|
||||
|
||||
Java 的 I/O 大概可以分成以下几类
|
||||
|
||||
1. 磁盘操作:File
|
||||
2. 字节操作:InputStream 和 OutputStream
|
||||
3. 字符操作:Reader 和 Writer
|
||||
4. 对象操作:Serializable
|
||||
5. 网络操作:Socket
|
||||
6. 非阻塞式 IO:NIO
|
||||
|
||||
# 磁盘操作
|
||||
|
||||
File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。
|
||||
|
||||
# 字节操作
|
||||
|
||||

|
||||
|
||||
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
|
||||
|
||||
```java
|
||||
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
|
||||
```
|
||||
|
||||
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
|
||||
|
||||
批量读入文件中的内容到字节数组中
|
||||
|
||||
```java
|
||||
byte[] buf = new byte[20*1024];
|
||||
int bytes = 0;
|
||||
// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof,即文件尾
|
||||
while((bytes = in.read(buf, 0 , buf.length)) != -1) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
# 字符操作
|
||||
|
||||
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的数据通常是字符形式,因此需要提供对字符进行操作的方法。
|
||||
|
||||
InputStreamReader 实现从文本文件的字节流解码成字符流;OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们都继承自 Reader 和 Writer。
|
||||
|
||||
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
|
||||
|
||||
```java
|
||||
byte[] bytes = str.getBytes(encoding); // 编码
|
||||
String str = new String(bytes, encoding); // 解码
|
||||
```
|
||||
|
||||
GBK 编码中,中文占 2 个字节,英文占 1 个字节;UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;Java 使用双字节编码 UTF-16be,中文和英文都占 2 个字节。
|
||||
|
||||
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
|
||||
|
||||
# 对象操作
|
||||
|
||||
序列化就是将一个对象转换成字节序列,方便存储和传输。
|
||||
|
||||
序列化:ObjectOutputStream.writeObject()
|
||||
|
||||
反序列化:ObjectInputStream.readObject()
|
||||
|
||||
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
|
||||
|
||||
transient 关键字可以使一些属性不会被序列化。
|
||||
|
||||
**ArrayList 序列化和反序列化的实现**:ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
|
||||
|
||||
```
|
||||
private transient Object[] elementData;
|
||||
```
|
||||
|
||||
# 网络操作
|
||||
|
||||
Java 中的网络支持:
|
||||
|
||||
1. InetAddress:用于表示网络上的硬件资源,即 IP 地址;
|
||||
2. URL:统一资源定位符,通过 URL 可以直接读取或者写入网络上的数据;
|
||||
3. Sockets:使用 TCP 协议实现网络通信;
|
||||
4. Datagram:使用 UDP 协议实现网络通信。
|
||||
|
||||
## 1. InetAddress
|
||||
|
||||
没有公有构造函数,只能通过静态方法来创建实例,比如 InetAddress.getByName(String host)、InetAddress.getByAddress(byte[] addr)。
|
||||
|
||||
## 2. URL
|
||||
|
||||
可以直接从 URL 中读取字节流数据
|
||||
|
||||
```java
|
||||
URL url = new URL("http://www.baidu.com");
|
||||
InputStream is = url.openStream(); // 字节流
|
||||
InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流
|
||||
BufferedReader br = new BufferedReader(isr);
|
||||
String line = br.readLine();
|
||||
while (line != null) {
|
||||
System.out.println(line);
|
||||
line = br.readLine();
|
||||
}
|
||||
br.close();
|
||||
isr.close();
|
||||
is.close();
|
||||
```
|
||||
|
||||
## 3. Sockets
|
||||
|
||||
Socket 通信模型
|
||||
|
||||

|
||||
|
||||
- ServerSocket:服务器端类
|
||||
- Socket:客户端类
|
||||
|
||||
服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
|
||||
|
||||
## 4. Datagram
|
||||
|
||||
- DatagramPacket:数据包类
|
||||
- DatagramSocket:通信类
|
||||
|
||||
# NIO
|
||||
|
||||
NIO 将最耗时的 I/O 操作 ( 即填充和提取缓冲区 ) 转移回操作系统,因而 不需要程序员去控制就可以极大地提高运行速度。
|
||||
|
||||
## 1. 流与块
|
||||
|
||||
I/O 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
|
||||
|
||||
面向流的 I/O 一次一个字节进行处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
|
||||
|
||||
一个面向块的 I/O 系统以块的形式处理数据,每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
|
||||
|
||||
I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在更面向流的系统中,处理速度也会更快。
|
||||
|
||||
## 2. 通道与缓冲区
|
||||
|
||||
### 2.1 通道
|
||||
|
||||
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
|
||||
|
||||
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道是双向的,可以用于读、写或者同时用于读写。
|
||||
|
||||
通道包括以下类型:
|
||||
|
||||
- FileChannel:从文件中读写数据;
|
||||
- DatagramChannel:通过 UDP 读写网络中数据;
|
||||
- SocketChannel:通过 TCP 读写网络中数据;
|
||||
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
|
||||
|
||||
### 2.2 缓冲区
|
||||
|
||||
发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是先经过缓冲区。
|
||||
|
||||
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
|
||||
|
||||
缓冲区包括以下类型:
|
||||
|
||||
- ByteBuffer
|
||||
- CharBuffer
|
||||
- ShortBuffer
|
||||
- IntBuffer
|
||||
- LongBuffer
|
||||
- FloatBuffer
|
||||
- DoubleBuffer
|
||||
|
||||
|
||||
## 3. 缓冲区状态变量
|
||||
|
||||
- capacity:最大容量;
|
||||
- position:当前已经读写的字节数;
|
||||
- limit:还可以读写的字节数。
|
||||
|
||||
状态变量的改变过程:
|
||||
|
||||
1\. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit == capacity == 9。capacity 变量不会改变,下面的讨论会忽略它。
|
||||
|
||||

|
||||
|
||||
2\. 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。
|
||||
|
||||

|
||||
|
||||
3\. 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
|
||||
|
||||

|
||||
|
||||
4\. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
|
||||
|
||||

|
||||
|
||||
5\. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
|
||||
|
||||

|
||||
|
||||
## 4. 读写文件实例
|
||||
|
||||
1\. 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel;
|
||||
|
||||
```java
|
||||
FileInputStream fin = new FileInputStream("readandshow.txt");
|
||||
FileChannel fic = fin.getChannel();
|
||||
```
|
||||
|
||||
2\. 创建一个容量为 1024 的 Buffer
|
||||
|
||||
```java
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
```
|
||||
|
||||
3\. 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话, read() 方法会返回 -1
|
||||
|
||||
```java
|
||||
int r = fcin.read(buffer);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
4\. 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel
|
||||
|
||||
```java
|
||||
FileOutputStream fout = new FileOutputStream("writesomebytes.txt");
|
||||
FileChannel foc = fout.getChannel();
|
||||
```
|
||||
|
||||
5\. 调用 flip() 切换读写
|
||||
|
||||
```java
|
||||
buffer.flip();
|
||||
```
|
||||
|
||||
6\. 把 Buffer 中的数据读取到输出 FileChannel 中
|
||||
|
||||
```java
|
||||
foc.write(buffer);
|
||||
```
|
||||
|
||||
7\. 最后调用 clear() 重置缓冲区
|
||||
|
||||
```java
|
||||
buffer.clear();
|
||||
```
|
||||
|
||||
## 5. 阻塞与非阻塞
|
||||
|
||||
应当注意,FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
|
||||
|
||||
### 5.1 阻塞式 I/O
|
||||
|
||||
阻塞式 I/O 在调用 InputStream.read() 方法时会一直等到数据到来时(或超时)才会返回,在调用 ServerSocket.accept() 方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。
|
||||
|
||||

|
||||
|
||||
### 5.2 非阻塞式 I/O
|
||||
|
||||
由一个专门的线程来处理所有的 I/O 事件,并负责分发。
|
||||
|
||||
事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
|
||||
|
||||
线程通信:线程之间通过 wait()、notify() 等方式通信,保证每次上下文切换都是有意义的,减少无谓的线程切换。
|
||||
|
||||

|
||||
|
||||
## 6. 套接字实例
|
||||
|
||||
### 6.1 ServerSocketChannel
|
||||
|
||||
每一个端口都需要有一个 ServerSocketChannel 用来监听连接。
|
||||
|
||||
```java
|
||||
ServerSocketChannel ssc = ServerSocketChannel.open();
|
||||
ssc.configureBlocking(false); // 设置为非阻塞
|
||||
|
||||
ServerSocket ss = ssc.socket();
|
||||
InetSocketAddress address = new InetSocketAddress(ports[i]);
|
||||
ss.bind(address); // 绑定端口号
|
||||
```
|
||||
|
||||
### 6.2 Selectors
|
||||
|
||||
异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。
|
||||
|
||||
创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。
|
||||
|
||||
SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
|
||||
|
||||
```java
|
||||
Selector selector = Selector.open();
|
||||
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
|
||||
```
|
||||
|
||||
### 6.3 主循环
|
||||
|
||||
首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。
|
||||
|
||||
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。
|
||||
|
||||
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
|
||||
|
||||
```java
|
||||
int num = selector.select();
|
||||
|
||||
Set selectedKeys = selector.selectedKeys();
|
||||
Iterator it = selectedKeys.iterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
SelectionKey key = (SelectionKey)it.next();
|
||||
// ... deal with I/O event ...
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 监听新连接
|
||||
|
||||
程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:
|
||||
|
||||
```java
|
||||
if ((key.readyOps() & SelectionKey.OP_ACCEPT)
|
||||
== SelectionKey.OP_ACCEPT) {
|
||||
// Accept the new connection
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
可以肯定地说, readOps() 方法告诉我们该事件是新的连接。
|
||||
|
||||
### 6.5 接受新的连接
|
||||
|
||||
因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:
|
||||
|
||||
```java
|
||||
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
|
||||
SocketChannel sc = ssc.accept();
|
||||
```
|
||||
|
||||
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:
|
||||
|
||||
```java
|
||||
sc.configureBlocking( false );
|
||||
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
|
||||
```
|
||||
|
||||
注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于 读取 而不是 接受 新连接。
|
||||
|
||||
### 6.6 删除处理过的 SelectionKey
|
||||
|
||||
在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey:
|
||||
|
||||
```java
|
||||
it.remove();
|
||||
```
|
||||
|
||||
现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。
|
||||
|
||||
### 6.7 传入的 I/O
|
||||
|
||||
当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:
|
||||
|
||||
```java
|
||||
} else if ((key.readyOps() & SelectionKey.OP_READ)
|
||||
== SelectionKey.OP_READ) {
|
||||
// Read the data
|
||||
SocketChannel sc = (SocketChannel)key.channel();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Eckel B, 埃克尔 , 昊鹏 , 等 . Java 编程思想 [M]. 机械工业出版社 , 2002.
|
||||
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
|
||||
- [ 深入分析 Java I/O 的工作机制 ](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
|
||||
- [NIO 与传统 IO 的区别 ](http://blog.csdn.net/shimiso/article/details/24990499)
|
@ -1,128 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 基础
|
||||
|
||||
## final
|
||||
|
||||
**final 数据**
|
||||
|
||||
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
|
||||
|
||||
对于基本类型,final 使数值不变;对于引用对象,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
|
||||
|
||||
**final 方法**
|
||||
|
||||
声明方法不能被子类覆盖。
|
||||
|
||||
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。
|
||||
|
||||
**final 类**
|
||||
|
||||
声明类不允许被继承。
|
||||
|
||||
## 初始化顺序
|
||||
|
||||
static 声明的静态数据在内存中只存在一份,只在类第一次实例化时初始化一次,优先于其它数据的初始化。
|
||||
|
||||
```java
|
||||
public static String staticField = "静态变量";
|
||||
```
|
||||
|
||||
static 语句块和 static 数据一样在类第一次实例化时运行一次,具体哪个先运行取决于它们在代码中的顺序。
|
||||
|
||||
```java
|
||||
static {
|
||||
System.out.println("静态初始化块");
|
||||
}
|
||||
```
|
||||
|
||||
普通数据和普通语句块的初始化在静态数据和静态语句块初始化结束之后。
|
||||
|
||||
```java
|
||||
public String field = "变量";
|
||||
```
|
||||
|
||||
```java
|
||||
{
|
||||
System.out.println("初始化块");
|
||||
}
|
||||
```
|
||||
|
||||
最后才是构造函数中的数据进行初始化
|
||||
|
||||
```java
|
||||
public InitialOrderTest()
|
||||
{
|
||||
System.out.println("构造器");
|
||||
}
|
||||
```
|
||||
|
||||
存在继承的情况下,初始化顺序为:
|
||||
|
||||
1. 父类(静态数据、静态语句块)
|
||||
2. 子类(静态数据、静态语句块)
|
||||
3. 父类(数据、语句块)
|
||||
4. 父类(构造器)
|
||||
5. 子类(数据、语句块)
|
||||
6. 子类(构造器)
|
||||
|
||||
## 访问权限
|
||||
|
||||
Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
|
||||
|
||||
可以对类或类中的成员(字段以及方法)加上访问修饰符。成员可见表示其它类可以用成员所在类的对象访问到该成员;类可见表示其它类可以用这个类创建对象,可以把类当做包中的一个成员,然后包表示一个类,这样就好理解了。
|
||||
|
||||
protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。
|
||||
|
||||
# 容器
|
||||
|
||||

|
||||
|
||||
容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。
|
||||
|
||||
## Set
|
||||
|
||||
- HashSet:使用 Hash 实现,支持快速查找,但是失去有序性;
|
||||
|
||||
- TreeSet:使用树实现,保持有序,但是查找效率不如 HashSet;
|
||||
|
||||
- LinkedListHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序,因此具有有序性。
|
||||
|
||||
## Queue
|
||||
|
||||
只有两个实现:LinkedList 和 PriorityQueue,其中 LinkedList 支持双向队列。
|
||||
|
||||
## Map
|
||||
|
||||
- HashMap:使用 Hash 实现
|
||||
|
||||
- LinkedHashMap:保持有序,顺序为插入顺序或者最近最少使用(LRU)顺序
|
||||
|
||||
- TreeMap:基于红黑树实现
|
||||
|
||||
- ConcurrentHashMap:线程安全 Map,不涉及同步加锁
|
||||
|
||||
# 反射
|
||||
|
||||
每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
|
||||
|
||||
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName('com.mysql.jdbc.Driver.class') 这种方式来控制类的加载,该方法会返回一个 Class 对象。
|
||||
|
||||
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
|
||||
|
||||
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库包含了 **Field**、**Method** 以及 **Constructor** 类。可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段,可以使用 invoke() 方法调用与 Method 对象关联的方法,可以用 Constructor 创建新的对象。
|
||||
|
||||
IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。
|
||||
|
||||
# 异常
|
||||
|
||||
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:**Error** 和 **Exception**,其中 Error 用来表示编译时系统错误。
|
||||
|
||||
Exception 分为两种:**受检异常** 和 **非受检异常**。受检异常需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;非受检异常是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。
|
||||
|
||||

|
||||
|
||||
# 参考资料
|
||||
|
||||
- Eckel B, 埃克尔 , 昊鹏 , 等 . Java 编程思想 [M]. 机械工业出版社 , 2002.
|
||||
- [Java 类初始化顺序 ](https://segmentfault.com/a/1190000004527951)
|
@ -1,341 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 概览
|
||||
|
||||

|
||||
|
||||
容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。
|
||||
|
||||
## 1. List
|
||||
|
||||
- ArrayList:基于动态数组实现,支持随机访问;
|
||||
|
||||
- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双端队列。
|
||||
|
||||
## 2. Set
|
||||
|
||||
- HashSet:基于 Hash 实现,支持快速查找,但是失去有序性;
|
||||
|
||||
- TreeSet:基于红黑树实现,保持有序,但是查找效率不如 HashSet;
|
||||
|
||||
- LinkedListHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序,因此具有有序性。
|
||||
|
||||
## 3. Queue
|
||||
|
||||
只有两个实现:LinkedList 和 PriorityQueue,其中 LinkedList 支持双向队列,PriorityQueue 是基于堆结构实现。
|
||||
|
||||
## 4. Map
|
||||
|
||||
- HashMap:基于 Hash 实现
|
||||
|
||||
- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序
|
||||
|
||||
- TreeMap:基于红黑树实现
|
||||
|
||||
- ConcurrentHashMap:线程安全 Map,不涉及类似于 HashTable 的同步加锁
|
||||
|
||||
## 5. Java 1.0/1.1 容器
|
||||
|
||||
对于旧的容器,我们决不应该使用它们,只需要对它们进行了解。
|
||||
|
||||
- Vector:和 ArrayList 类似,但它是线程安全的
|
||||
|
||||
- HashTable:和 HashMap 类似,但它是线程安全的
|
||||
|
||||
# 容器中的设计模式
|
||||
|
||||
## 1. 迭代器模式
|
||||
|
||||
从概览图可以看到,每个集合类都有一个 Iterator 对象,可以通过这个迭代器对象来遍历集合中的元素。
|
||||
|
||||
[Java 中的迭代器模式 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#92-java-%E5%86%85%E7%BD%AE%E7%9A%84%E8%BF%AD%E4%BB%A3%E5%99%A8)
|
||||
|
||||
## 2. 适配器模式
|
||||
|
||||
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
|
||||
|
||||
```java
|
||||
List list = Arrays.asList(1, 2, 3);
|
||||
int[] arr = {1, 2, 3};
|
||||
list = Arrays.asList(arr);
|
||||
```
|
||||
|
||||
# 散列
|
||||
|
||||
使用 hasCode() 来返回散列值,使用的是对象的地址。
|
||||
|
||||
而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。
|
||||
|
||||
相等必须满足以下五个性质:
|
||||
|
||||
1. 自反性
|
||||
2. 对称性
|
||||
3. 传递性
|
||||
4. 一致性(多次调用 x.equals(y),结果不变)
|
||||
5. 对任何不是 null 的对象 x 调用 x.equals(nul) 结果都为 false
|
||||
|
||||
# 源码分析
|
||||
|
||||
建议先阅读 [ 算法 - 查找 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E7%AC%AC%E4%B8%89%E7%AB%A0-%E6%9F%A5%E6%89%BE) 部分,对集合类源码的理解有很大帮助。
|
||||
|
||||
源码下载:[OpenJDK 1.7](http://download.java.net/openjdk/jdk7)
|
||||
|
||||
## 1. ArraList
|
||||
|
||||
[ArraList.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/ArrayList.java)
|
||||
|
||||
实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
|
||||
|
||||
```java
|
||||
public class ArrayList<E> extends AbstractList<E>
|
||||
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
|
||||
```
|
||||
|
||||
基于数组实现,保存元素的数组使用 transient 修饰,这是因为该数组不一定所有位置都占满元素,因此也就没必要全部都进行序列化。需要重写 writeObject() 和 readObject()。
|
||||
|
||||
```java
|
||||
private transient Object[] elementData;
|
||||
```
|
||||
|
||||
数组的默认大小为 10
|
||||
|
||||
```java
|
||||
public ArrayList(int initialCapacity) {
|
||||
super();
|
||||
if (initialCapacity < 0)
|
||||
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
|
||||
this.elementData = new Object[initialCapacity];
|
||||
}
|
||||
|
||||
public ArrayList() {
|
||||
this(10);
|
||||
}
|
||||
```
|
||||
|
||||
删除元素时调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高,最好在创建时就指定大概的容量大小,减少复制操作的执行次数。
|
||||
|
||||
```java
|
||||
public E remove(int index) {
|
||||
rangeCheck(index);
|
||||
|
||||
modCount++;
|
||||
E oldValue = elementData(index);
|
||||
|
||||
int numMoved = size - index - 1;
|
||||
if (numMoved > 0)
|
||||
System.arraycopy(elementData, index+1, elementData, index, numMoved);
|
||||
elementData[--size] = null; // Let gc do its work
|
||||
|
||||
return oldValue;
|
||||
}
|
||||
```
|
||||
|
||||
添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要进行扩容,使得新容量为旧容量的 1.5 倍。
|
||||
|
||||
modCount 用来记录 ArrayList 发生变化的次数,因为每次在进行 add() 和 addAll() 时都需要调用 ensureCapacity(),因此直接在 ensureCapacity() 中对 modCount 进行修改。
|
||||
|
||||
```java
|
||||
public void ensureCapacity(int minCapacity) {
|
||||
if (minCapacity > 0)
|
||||
ensureCapacityInternal(minCapacity);
|
||||
}
|
||||
|
||||
private void ensureCapacityInternal(int minCapacity) {
|
||||
modCount++;
|
||||
// overflow-conscious code
|
||||
if (minCapacity - elementData.length > 0)
|
||||
grow(minCapacity);
|
||||
}
|
||||
|
||||
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
private void grow(int minCapacity) {
|
||||
// overflow-conscious code
|
||||
int oldCapacity = elementData.length;
|
||||
int newCapacity = oldCapacity + (oldCapacity >> 1);
|
||||
if (newCapacity - minCapacity < 0)
|
||||
newCapacity = minCapacity;
|
||||
if (newCapacity - MAX_ARRAY_SIZE > 0)
|
||||
newCapacity = hugeCapacity(minCapacity);
|
||||
// minCapacity is usually close to size, so this is a win:
|
||||
elementData = Arrays.copyOf(elementData, newCapacity);
|
||||
}
|
||||
|
||||
private static int hugeCapacity(int minCapacity) {
|
||||
if (minCapacity < 0) // overflow
|
||||
throw new OutOfMemoryError();
|
||||
return (minCapacity > MAX_ARRAY_SIZE) ?
|
||||
Integer.MAX_VALUE :
|
||||
MAX_ARRAY_SIZE;
|
||||
}
|
||||
```
|
||||
|
||||
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
|
||||
|
||||
```java
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException{
|
||||
// Write out element count, and any hidden stuff
|
||||
int expectedModCount = modCount;
|
||||
s.defaultWriteObject();
|
||||
|
||||
// Write out array length
|
||||
s.writeInt(elementData.length);
|
||||
|
||||
// Write out all elements in the proper order.
|
||||
for (int i=0; i<size; i++)
|
||||
s.writeObject(elementData[i]);
|
||||
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
**和 Vector 的区别**
|
||||
|
||||
1. Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问要慢。最好使用 ArrayList 而不是 Vector,因为同步完全可以由程序员自己来控制;
|
||||
2. Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
|
||||
|
||||
为了使用线程安全的 ArrayList,可以使用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
|
||||
|
||||
**和 LinkedList 的区别**
|
||||
|
||||
1. ArrayList 基于动态数组实现,LinkedList 基于双向循环链表实现;
|
||||
2. ArrayList 支持随机访问,LinkedList 不支持;
|
||||
3. LinkedList 在任意位置添加删除元素更快。
|
||||
|
||||
## 2. Vector 与 Stack
|
||||
|
||||
[Vector.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/Vector.java)
|
||||
|
||||
## 3. LinkedList
|
||||
|
||||
[LinkedList.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/LinkedList.java)
|
||||
|
||||
## 4. TreeMap
|
||||
|
||||
[TreeMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/TreeMap.java)
|
||||
|
||||
## 5. HashMap
|
||||
|
||||
[HashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java)
|
||||
|
||||
使用拉链法来解决冲突。
|
||||
|
||||
默认容量 capacity 为 16,需要注意的是容量必须保证为 2 的次方。容量就是 Entry[] table 数组的长度,size 是数组的实际使用量。
|
||||
|
||||
threshold 规定了一个 size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。
|
||||
|
||||
threshold = capacity * load_factor,其中 load_factor 为 table 数组能够使用的比例,load_factor 过大会导致聚簇的出现,从而影响查询和插入的效率,详见算法笔记。
|
||||
|
||||
```java
|
||||
static final int DEFAULT_INITIAL_CAPACITY = 16;
|
||||
|
||||
static final int MAXIMUM_CAPACITY = 1 << 30;
|
||||
|
||||
static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
||||
|
||||
transient Entry[] table;
|
||||
|
||||
transient int size;
|
||||
|
||||
int threshold;
|
||||
|
||||
final float loadFactor;
|
||||
|
||||
transient int modCount;
|
||||
```
|
||||
|
||||
从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。
|
||||
|
||||
```java
|
||||
void addEntry(int hash, K key, V value, int bucketIndex) {
|
||||
Entry<K,V> e = table[bucketIndex];
|
||||
table[bucketIndex] = new Entry<>(hash, key, value, e);
|
||||
if (size++ >= threshold)
|
||||
resize(2 * table.length);
|
||||
}
|
||||
```
|
||||
|
||||
Entry 用来表示一个键值对元素,其中的 next 指针在序列化时会使用。
|
||||
|
||||
```java
|
||||
static class Entry<K,V> implements Map.Entry<K,V> {
|
||||
final K key;
|
||||
V value;
|
||||
Entry<K,V> next;
|
||||
final int hash;
|
||||
}
|
||||
```
|
||||
|
||||
get() 操作需要分成两种情况,key 为 null 和 不为 null,从中可以看出 HashMap 允许插入 null 作为键。
|
||||
|
||||
```java
|
||||
public V get(Object key) {
|
||||
if (key == null)
|
||||
return getForNullKey();
|
||||
int hash = hash(key.hashCode());
|
||||
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
|
||||
Object k;
|
||||
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
|
||||
return e.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
put() 操作也需要根据 key 是否为 null 做不同的处理,需要注意的是如果本来没有 key 为 null 的键值对,新插入一个 key 为 null 的键值对时默认是放在数组的 0 位置,这是因为 null 不能计算 hash 值,也就无法知道应该放在哪个链表上。
|
||||
|
||||
```java
|
||||
public V put(K key, V value) {
|
||||
if (key == null)
|
||||
return putForNullKey(value);
|
||||
int hash = hash(key.hashCode());
|
||||
int i = indexFor(hash, table.length);
|
||||
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
|
||||
Object k;
|
||||
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
|
||||
V oldValue = e.value;
|
||||
e.value = value;
|
||||
e.recordAccess(this);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
modCount++;
|
||||
addEntry(hash, key, value, i);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
private V putForNullKey(V value) {
|
||||
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
|
||||
if (e.key == null) {
|
||||
V oldValue = e.value;
|
||||
e.value = value;
|
||||
e.recordAccess(this);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
modCount++;
|
||||
addEntry(0, null, value, 0);
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. LinkedHashMap
|
||||
|
||||
[LinkedHashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java)
|
||||
|
||||
## 7. ConcurrentHashMap
|
||||
|
||||
[ConcurrentHashMap.java](https://github.com/CyC2018/InterviewNotes/blob/master/notes/src/HashMap.java)
|
||||
|
||||
[ 探索 ConcurrentHashMap 高并发性的实现机制 ](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Java 编程思想
|
@ -1,536 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 使用线程
|
||||
|
||||
有三种使用线程的方法:
|
||||
|
||||
1. 实现 Runnable 接口;
|
||||
2. 实现 Callable 接口;
|
||||
3. 继承 Tread 类;
|
||||
|
||||
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要 Thread 来调用。可以说任务是通过线程驱动从而执行的。
|
||||
|
||||
## 1. 实现 Runnable 接口
|
||||
|
||||
有一个 run() 方法需要实现
|
||||
|
||||
需要 Thread 调用 start() 来启动线程
|
||||
|
||||
```java
|
||||
public class MyRunnable implements Runnable {
|
||||
public void run() {
|
||||
// ...
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
MyRunnable instance = new MyRunnable();
|
||||
Tread thread = new Thread(instance);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 实现 Callable 接口
|
||||
|
||||
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
|
||||
|
||||
```java
|
||||
public class MyCallable implements Callable<Integer> {
|
||||
public Integer call() {
|
||||
// ...
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
MyCallable mc = new MyCallable();
|
||||
FutureTask<Integer> ft = new FutureTask<>(mc);
|
||||
Thread thread = new Thread(ft);
|
||||
thread.start();
|
||||
System.out.println(ft.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 继承 Tread 类
|
||||
|
||||
同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
|
||||
|
||||
```java
|
||||
class MyThread extends Thread {
|
||||
public void run() {
|
||||
// ...
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
MyThread mt = new MyThread();
|
||||
mt.start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 实现接口 vs 继承 Thread
|
||||
|
||||
实现接口会更好一些,因为:
|
||||
|
||||
1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口。
|
||||
2. 类可能只要求可执行即可,继承整个 Thread 类开销会过大。
|
||||
|
||||
# Executor
|
||||
|
||||
Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。
|
||||
|
||||
主要有三种 Excutor:
|
||||
|
||||
1. CachedTreadPool:一个任务创建一个线程;
|
||||
2. FixedThreadPool:所有任务只能使用固定大小的线程;
|
||||
3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
|
||||
|
||||
```java
|
||||
ExecutorService exec = Executors.newCachedThreadPool();
|
||||
for(int i = 0; i < 5; i++) {
|
||||
exec.execute(new MyRunnable());
|
||||
}
|
||||
```
|
||||
|
||||
# 基础线程机制
|
||||
|
||||
## 1. sleep()
|
||||
|
||||
**Thread.sleep(millisec)** 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。
|
||||
|
||||
sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
try {
|
||||
// ...
|
||||
Thread.sleep(1000);
|
||||
// ...
|
||||
} catch(InterruptedException e) {
|
||||
System.err.println(e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. yield()
|
||||
|
||||
对静态方法 **Thread.yield()** 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
// ...
|
||||
Thread.yield();
|
||||
}
|
||||
```
|
||||
|
||||
## 3. join()
|
||||
|
||||
在线程中调用另一个线程的 **join()** 方法,会将当前线程挂起,直到目标线程结束。
|
||||
|
||||
可以加一个超时参数。
|
||||
|
||||
## 4. deamon
|
||||
|
||||
后台线程(**deamon**)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。
|
||||
|
||||
当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。
|
||||
|
||||
main() 属于非后台线程。
|
||||
|
||||
使用 setDaemon() 方法将一个线程设置为后台线程。
|
||||
|
||||
# 线程之间的协作
|
||||
|
||||
- **线程通信**:保证线程以一定的顺序执行;
|
||||
- **线程同步**:保证线程对临界资源的互斥访问。
|
||||
|
||||
线程通信往往是基于线程同步的基础上完成的,因此很多线程通信问题也是线程同步问题。
|
||||
|
||||
## 1. 线程通信
|
||||
|
||||
**wait()、notify() 和 notifyAll()** 三者实现了线程之间的通信。
|
||||
|
||||
wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。
|
||||
|
||||
sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。
|
||||
|
||||
这几个方法属于基类的一部分,而不属于 Thread。
|
||||
|
||||
```java
|
||||
private boolean flag = false;
|
||||
|
||||
public synchronized void after() {
|
||||
while(flag == false) {
|
||||
wait();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void before() {
|
||||
flag = true;
|
||||
notifyAll();
|
||||
}
|
||||
```
|
||||
|
||||
**wait() 和 sleep() 的区别**
|
||||
|
||||
1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法;
|
||||
2. wait() 会放弃锁,而 sleep() 不会。
|
||||
|
||||
## 2. 线程同步
|
||||
|
||||
给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。
|
||||
|
||||
### 2.1 synchronized
|
||||
|
||||
**同步一个方法**
|
||||
|
||||
使多个线程不能同时访问该方法。
|
||||
|
||||
```java
|
||||
public synchronized void func(String name) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**同步一个代码块**
|
||||
|
||||
```java
|
||||
public void func(String name) {
|
||||
synchronized(this) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Lock
|
||||
|
||||
若要实现更细粒度的控制,我们可以使用锁(lock)。
|
||||
|
||||
```java
|
||||
private Lock lock;
|
||||
public int func(int value) {
|
||||
lock.lock();
|
||||
// ...
|
||||
lock.unlock();
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 BlockingQueue
|
||||
|
||||
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
|
||||
|
||||
- **FIFO 队列**:LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
|
||||
- **优先级队列**:PriorityBlockingQueue
|
||||
|
||||
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将一直阻塞到队列中有内容,如果队列为满 put() 将阻塞到队列有空闲位置。它们响应中断,当收到中断请求的时候会抛出 InterruptedException,从而提前结束阻塞状态。
|
||||
|
||||
**使用 BlockingQueue 实现生产者消费者问题**
|
||||
|
||||
```java
|
||||
// 生产者
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class Producer implements Runnable {
|
||||
private BlockingQueue<String> queue;
|
||||
|
||||
public Producer(BlockingQueue<String> queue) {
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println(Thread.currentThread().getName() + " is making product...");
|
||||
String product = "made by " + Thread.currentThread().getName();
|
||||
try {
|
||||
queue.put(product);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// 消费者
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class Consumer implements Runnable{
|
||||
private BlockingQueue<String> queue;
|
||||
|
||||
public Consumer(BlockingQueue<String> queue) {
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
String product = queue.take();
|
||||
System.out.println(Thread.currentThread().getName() + " is consuming product " + product + "...");
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// 客户端
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
public class Client {
|
||||
public static void main(String[] args) {
|
||||
BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
new Thread(new Consumer(queue), "Producer" + i).start();
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// 只有两个 Product,因此只能消费两个,其它三个消费者被阻塞
|
||||
new Thread(new Producer(queue), "Consumer" + i).start();
|
||||
}
|
||||
for (int i = 2; i < 5; i++) {
|
||||
new Thread(new Consumer(queue), "Producer" + i).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
// 运行结果
|
||||
Consumer0 is making product...
|
||||
Producer0 is consuming product made by Consumer0...
|
||||
Consumer1 is making product...
|
||||
Producer1 is consuming product made by Consumer1...
|
||||
Consumer2 is making product...
|
||||
Consumer3 is making product...
|
||||
Consumer4 is making product...
|
||||
Producer2 is consuming product made by Consumer2...
|
||||
Producer3 is consuming product made by Consumer3...
|
||||
Producer4 is consuming product made by Consumer4...
|
||||
```
|
||||
|
||||
# 线程状态
|
||||
|
||||
JDK 从 1.5 开始在 Thread 类中增添了 State 枚举,包含以下六种状态:
|
||||
|
||||
1. **NEW**(新建)
|
||||
2. **RUNNABLE**(当线程正在运行或者已经就绪正等待 CPU 时间片)
|
||||
3. **BLOCKED**(阻塞,线程在等待获取对象同步锁)
|
||||
4. **Waiting**(调用不带超时的 wait() 或 join())
|
||||
5. **TIMED_WAITING**(调用 sleep()、带超时的 wait() 或者 join())
|
||||
6. **TERMINATED**(死亡)
|
||||
|
||||

|
||||
|
||||
# 结束线程
|
||||
|
||||
## 1. 阻塞
|
||||
|
||||
一个线程进入阻塞状态可能有以下原因:
|
||||
|
||||
1. 调用 Thread.sleep() 方法进入休眠状态;
|
||||
2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息;
|
||||
3. 等待某个 I/O 的完成;
|
||||
4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。
|
||||
|
||||
## 2. 中断
|
||||
|
||||
使用中断机制即可终止阻塞的线程。
|
||||
|
||||
使用 **interrupt()** 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。
|
||||
|
||||
应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。
|
||||
|
||||
**不可中断的阻塞**
|
||||
|
||||
不能中断 I/O 阻塞和 synchronized 锁阻塞。
|
||||
|
||||
**Executor 的中断操作**
|
||||
|
||||
Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断所有它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。
|
||||
|
||||
如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。
|
||||
|
||||
**检查中断**
|
||||
|
||||
通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。
|
||||
|
||||
interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。
|
||||
|
||||
interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
|
||||
|
||||
# 原子性
|
||||
|
||||
对于除 long 和 double 之外的基本类型变量的读写,可以看成是具有原子性的,以不可分割的步骤操作内存。
|
||||
|
||||
JVM 将 64 位变量(long 和 double)的读写当做两个分离的 32 位操作来执行,在两个操作之间可能会发生上下文切换,因此不具有原子性。可以使用 **volatile** 关键字来定义 long 和 double 变量,从而获得原子性。
|
||||
|
||||
**AtomicInteger、AtomicLong、AtomicReference** 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。
|
||||
|
||||
```java
|
||||
boolean compareAndSet(expectedValue, updateValue);
|
||||
```
|
||||
|
||||
AtomicInteger 使用举例:
|
||||
|
||||
```java
|
||||
private AtomicInteger ai = new AtomicInteger(0);
|
||||
|
||||
public int next() {
|
||||
return ai.addAndGet(2)
|
||||
}
|
||||
```
|
||||
|
||||
原子性具有很多复杂问题,应当尽量使用同步而不是原子性。
|
||||
|
||||
# volatile
|
||||
|
||||
保证了内存可见性和禁止指令重排,没法保证原子性。
|
||||
|
||||
## 1. 内存可见性
|
||||
|
||||
普通共享变量被修改之后,什么时候被写入主存是不确定的。
|
||||
|
||||
volatile 关键字会保证每次修改共享变量之后该值会立即更新到内存中,并且在读取时会从内存中读取值。
|
||||
|
||||
synchronized 和 Lock 也能够保证内存可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized,那么 get() 方法并不能保证会从内存中读取最新的数据。
|
||||
|
||||
## 2. 禁止指令重排
|
||||
|
||||
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
|
||||
|
||||
volatile 关键字通过添加内存屏障的方式来进制指令重排,即重排序时不能把后面的指令放到内存屏障之前。
|
||||
|
||||
可以通过 synchronized 和 Lock 来保证有序性,它们保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
|
||||
|
||||
# 可重入内置锁
|
||||
|
||||
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
|
||||
|
||||
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。
|
||||
|
||||
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:
|
||||
|
||||
```java
|
||||
public class Father
|
||||
{
|
||||
public synchronized void doSomething(){
|
||||
......
|
||||
}
|
||||
}
|
||||
|
||||
public class Child extends Father
|
||||
{
|
||||
public synchronized void doSomething(){
|
||||
......
|
||||
super.doSomething();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。
|
||||
|
||||
由于Fither和Child中的doSomething()方法都是synchronized方法,因此每个doSomething()方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething()时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
|
||||
|
||||
同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。
|
||||
|
||||
|
||||
# Concurrent
|
||||
|
||||
**Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。**
|
||||
|
||||
CyclicBarrier 和 CountDownLatch 区别
|
||||
|
||||
这两个类非常类似,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:
|
||||
|
||||
* CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值 -1 而已,该线程继续运行
|
||||
|
||||
* CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
|
||||
|
||||
* CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch 就不可再用了
|
||||
|
||||
# 免锁容器
|
||||
|
||||
免锁容器通用策略是:对容器的修改可以与读取操作同时发生,只要读取这只能看到完成修改地结果即可。修改实在容器数据结构的某个部分的一个单独的副本(又是是整个数据结构的副本)上执行的,并且这个副本在修改过程是不可视的。只有当修改完成后,被修改地结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改版本了。
|
||||
|
||||
**CopyOnWriteArrayList** 和 **CopyOnWriteArraySet** 在写入时都会导致创建整个底层数组的副本,而 **ConcurrentHashMap** 和 **ConcurrentLinkedQueue** 只会创建部分副本。
|
||||
|
||||
1).Java.util 包中:
|
||||
HashMap、 HashSet、 ArrayList 都不是线程安全的, Vector、 HashTable 是线程
|
||||
安全的。通过 Collections.synchronizedList/Set/Map/SortedSet/SortedMap 可以返回
|
||||
一个同步的对应集合
|
||||
通过对每一个公共方法进行同步实现线程安全。但是在多线程环境下对它们
|
||||
进行诸如“缺少即加入”这类符合操作时, 就会出现问题。【解决】: 客户端加锁,
|
||||
要注意锁定的对象是对 List 等集合类,而不是客户端类
|
||||
【ConcurrentModificationException】
|
||||
使用 Vector 或者同步的 List,返回的 Iterator 迭代器都是 fail-fast 的, 这意味
|
||||
着如果在迭代过程中, 任何其他线程修改 List,都会失败, 抛出上异常,若想避
|
||||
免该异常则必须在迭代期间对容器加锁。
|
||||
2).Java.util.concurrent 包:
|
||||
以下类在迭代期间都无需对容器加锁
|
||||
CopyOnWriteArrayList 、 CopyOnWriteArraySet 、 ConcurrentHashMap 、
|
||||
ConcurrentLinkedQueue 、 ConcurrentSkipListMap( 替 代 SortedMap) 、
|
||||
ConcurrentSkipListSet( 替代 SortedSet)
|
||||
CopyOnWriteArrayList:底层维护一个 volatile 的基础数组, 某线程在对容器
|
||||
修改的时候先显示获取锁(只能有一个线程修改), 然后复制基础数组(保证了
|
||||
其他读线程不会出错), 最后更新原数组(因为是 volatile 的,所以对其他读线程
|
||||
立即可见)。 因为复制数组开销较大,所以适合元素少,修改操作少的情况。
|
||||
|
||||
## HashTable、HashMap、ConcurrentHashMap 的区别
|
||||
|
||||
1). 主要区别:
|
||||
1). 直观使用上: HashTable 不接受 key 为 null, HashMap 接受 key 为 null
|
||||
2). 哈希冲突的概率不同: 根据 Hash 值计算数组下标的算法不同, HashTable
|
||||
直接使用对象的 hashCode, hashMap 做了重新计算, HashTable 的冲突几率比
|
||||
HashMap 高
|
||||
hashTable 默认的数组大小为 11, hashMap 默认数组大小为 16,他们的默认
|
||||
负载因子都是 0.75, HashTable 扩展为 2*size+1, HashMap 扩展为 2*size
|
||||
3). 线程安全: HashTable 是线程安全的, HashMap 则不是线程安全的 , 但是
|
||||
仅仅是 Hashtable 仅仅是对每个方法进行了 synchronized 同步, 首先这样的效率
|
||||
会比较低;其次它本身的同步并不能保证程序在并发操作下的正确性(虽然每个
|
||||
方法都是同步的,但客户端调用可能在一个操作中调用多个方法,就不能保证操
|
||||
作原子性了),需要高层次的并发保护。
|
||||
2).ConcurrentHashMap 改进:
|
||||
|
||||
并发效率问题: Hashtable 和 Collections.synchronizedMap 通过同步每个方
|
||||
法获得线程安全。 即当一个线程执行一个 Map 方法时,无论其他线程要对 Map
|
||||
进行什么样操作,都不能执行,直到第一个线程结束才可以。对比来说,
|
||||
ConcurrentHashMap 所使用的锁分段技术,首先将数据分成一段一段的存储,然
|
||||
后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他
|
||||
段的数据也能被其他线程访问。 从而可以有效提高并发效率。
|
||||
|
||||
迭代问题: ConcurrentHashMap 返回的 iterator 是弱一致性的,并不会抛出
|
||||
ConcurrentModifiedException。 弱一致性的迭代器可以容许并发修改,迭代器可以
|
||||
感应到迭代器在被创建后,对容器的修改。
|
||||
增加了常见的原子操作 API: 它的 API 中包含一些原子形式的“putIfAbsent()、
|
||||
相等便移除、 相等便替换”这些在 HashTable 中非原子操作
|
||||
|
||||
## ConcurrentHashMap 源码分析
|
||||
|
||||
[探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
|
||||
|
||||
# ThreadLocal
|
||||
|
||||
重点:ThreadLocal的设计理念与作用,ThreadPool用法与优势
|
||||
|
||||
[ThreadLocal 源码深入分析 ](http://www.sczyh30.com/posts/Java/java-concurrent-threadlocal/)
|
||||
|
||||
# 线程池
|
||||
|
||||
**线程池的底层实现和工作原理(建议写一个雏形简版源码实现)**
|
||||
|
||||
[线程、多线程与线程池总结](https://www.jianshu.com/p/b8197dd2934c)
|
||||
|
||||
# 多线程开发良好的实践
|
||||
|
||||
- 给线程命名;
|
||||
- 最小化同步范围;
|
||||
- 优先使用 volatile;
|
||||
- 尽可能使用更高层次的并发工具而非 wait 和 notify() 来实现线程通信,如 BlockingQueue, Semeaphore;
|
||||
- 多用并发容器,少用同步容器,并发容器壁同步容器的可扩展性更好。
|
||||
- 考虑使用线程池
|
||||
- 最低限度的使用同步和锁,缩小临界区。因此相对于同步方法,同步块会更好。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Java 编程思想
|
||||
- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
|
||||
- [Java 面试专题 - 多线程 & 并发编程 ](https://www.jianshu.com/p/e0c8d3dced8a)
|
||||
- [可重入内置锁](https://github.com/francistao/LearningNotes/blob/master/Part2/JavaConcurrent/%E5%8F%AF%E9%87%8D%E5%85%A5%E5%86%85%E7%BD%AE%E9%94%81.md)
|
@ -1 +0,0 @@
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
||||
## 孤儿进程和僵死进程
|
||||
|
||||
### 孤儿进程
|
||||
|
||||
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害
|
||||
|
||||
### 僵尸进程
|
||||
|
||||
一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
|
||||
|
||||
僵死进程通过 ps 命令显示出来的状态为 Z。
|
||||
|
||||
系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
|
||||
|
||||
要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
|
||||
|
||||
- [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/)
|
@ -1,987 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 常用操作以及概念
|
||||
|
||||
## 求助
|
||||
|
||||
**1. --help**
|
||||
|
||||
指令的基本用法与选项介绍。
|
||||
|
||||
**2. man**
|
||||
|
||||
man 是 manual 的缩写,将指令的具体信息显示出来。
|
||||
|
||||
当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下:
|
||||
|
||||
| 代号 | 类型 |
|
||||
| -- | -- |
|
||||
| 1 | 用户在 shell 环境中可以操作的指令或者可执行文件 |
|
||||
| 5 | 配置文件 |
|
||||
| 8 | 系统管理员可以使用的管理指令 |
|
||||
|
||||
**3. info**
|
||||
|
||||
info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。
|
||||
|
||||
## 关机
|
||||
|
||||
**1. 数据同步写入磁盘 sync**
|
||||
|
||||
为了加快对磁盘上文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。
|
||||
|
||||
**2. shutdown**
|
||||
|
||||
```html
|
||||
# /sbin/shutdown [-krhc] [时间] [警告讯息]
|
||||
-k : 不会关机,只是发送警告讯息,通知所有在线的用户
|
||||
-r : 将系统的服务停掉后就重新启动
|
||||
-h : 将系统的服务停掉后就立即关机
|
||||
-c : 取消已经在进行的 shutdown 指令内容
|
||||
```
|
||||
|
||||
**3. 其它关机指令**
|
||||
|
||||
reboot、halt、poweroff。
|
||||
|
||||
## 查看进程
|
||||
|
||||
ps 指令。
|
||||
|
||||
例如查看 theadx 的相关信息:
|
||||
|
||||
```html
|
||||
ps aux | grep threadx
|
||||
```
|
||||
|
||||
## 查看端口
|
||||
|
||||
netstat 指令。
|
||||
|
||||
例如查看端口 80 是否被占用:
|
||||
|
||||
```html
|
||||
netstat -anp | grep 80
|
||||
```
|
||||
|
||||
## PATH
|
||||
|
||||
可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。
|
||||
|
||||
```html
|
||||
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
|
||||
```
|
||||
|
||||
## 运行等级
|
||||
|
||||
0:关机模式
|
||||
1:单用户模式(可用于破解root密码)
|
||||
2:无网络支持的多用户模式
|
||||
3:有网络支持的多用户模式(文本模式,工作中最常用的模式)
|
||||
4:保留,未使用
|
||||
5:有网络支持的 X-windows 支持多用户模式(桌面)
|
||||
6:重新引导系统,即重启
|
||||
|
||||
## sudo
|
||||
|
||||
使用 sudo 允许一般用户使用 root 可执行的命令。用户必须在 /etc/sudoers 中才能使用该指令。
|
||||
|
||||
## GNU
|
||||
|
||||
GNU 计划,又译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容:
|
||||
|
||||
- 以任何目的运行此程序的自由;
|
||||
- 再复制的自由;
|
||||
- 改进此程序,并公开发布改进的自由。
|
||||
|
||||
## 包管理工具
|
||||
|
||||
RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具- DPKG,全称为 Debian Package,功能方面与 RPM 相似。
|
||||
|
||||
YUM 基于 RPM 包管理工具,能够从指定的源空间(服务器,本地目录等)自动下载目标 RPM 包并且安装,具有依赖管理功能,可以自动处理依赖关系并进行下载、安装,无须繁琐地手动下载、安装每一个需要的依赖包。此外,YUM 的另一个功能是进行系统中所有软件的升级。
|
||||
|
||||
## 常见发行版本
|
||||
|
||||
Linux 发行版是预先集成好的 Linux 内核及各种应用软件。
|
||||
|
||||
**基于 DPKG**
|
||||
|
||||
商业发行版
|
||||
|
||||
- Ubuntu
|
||||
|
||||
社区发行版
|
||||
|
||||
- Debian
|
||||
|
||||
**基于 RPM**
|
||||
|
||||
商业发行版
|
||||
|
||||
- Red Hat
|
||||
|
||||
社区发行版
|
||||
|
||||
- Fedora
|
||||
- CentOS
|
||||
|
||||
# 分区
|
||||
|
||||
## 磁盘的文件名
|
||||
|
||||
Linux 中每个硬件都被当做一个文件。
|
||||
|
||||
常见磁盘的文件名:
|
||||
|
||||
- SCSI/SATA/USB 磁盘:/dev/sd[a-p]
|
||||
- IDE 磁盘:/dev/hd[a-d]
|
||||
|
||||
其中文件名后面的序号的确定与磁盘插入的顺序有关,而与磁盘所插入的插槽位置无关。
|
||||
|
||||
## 分区表
|
||||
|
||||
磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。
|
||||
|
||||
### 1. MBR
|
||||
|
||||
MBR 中,第一个扇区最重要,里面有:主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,partition table 占 64 bytes。
|
||||
|
||||
分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它空间用来记录分区表,可以记录更多的分区,因此通过扩展分区可以分出更多区分,这些分区称为逻辑分区。
|
||||
|
||||
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名+编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。
|
||||
|
||||
### 2. GPT
|
||||
|
||||
不同的磁盘有不同的扇区大小,例如 512bytes 和最新磁盘的 4k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA)。
|
||||
|
||||
GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。
|
||||
|
||||
GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。
|
||||
|
||||

|
||||
|
||||
## 开机检测程序
|
||||
|
||||
### 1. BIOS
|
||||
|
||||
BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序的会加载操作系统的核心文件。
|
||||
|
||||
MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
|
||||
|
||||

|
||||
|
||||
安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
|
||||
|
||||
### 2. UEFI
|
||||
|
||||
UEFI 相比于 BIOS 来说功能更为全面,也更为安全。
|
||||
|
||||
## 挂载
|
||||
|
||||
挂载利用目录作为分区的进入点,也就是说,进入目录之后就可以读取分区的数据。
|
||||
|
||||

|
||||
|
||||
# 文件权限与目录配置
|
||||
|
||||
## 文件权限概念
|
||||
|
||||
把用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。
|
||||
|
||||
使用 ls 查看一个文件时,会出现一个文件的信息,例如 drwxr-xr-x. 3 root root 17 May 6 00:14 .config,对这个内容的解释如下:
|
||||
|
||||
- drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段。
|
||||
- 3:链接数;
|
||||
- root:文件拥有者;
|
||||
- root:所属群组;
|
||||
- 17:文件大小;
|
||||
- May 6 00:14:文件最后被修改的时间;
|
||||
- .config:文件名。
|
||||
|
||||
常见的文件类型及其含义有:
|
||||
|
||||
- d:目录;
|
||||
- -:文件;
|
||||
- l:链接文件;
|
||||
|
||||
9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
|
||||
|
||||
## 文件属性以及权限的修改
|
||||
|
||||
### 1. 修改文件所属群组
|
||||
|
||||
```html
|
||||
# chgrp [-R] groupname dirname/filename
|
||||
-R:递归修改
|
||||
```
|
||||
|
||||
### 2. 修改文件拥有者
|
||||
|
||||
不仅可以修改文件拥有者,也可以修改文件所属群组。
|
||||
|
||||
```html
|
||||
# chown [-R] 用户名:群组名 dirname/filename
|
||||
```
|
||||
|
||||
### 3. 修改权限
|
||||
|
||||
可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r:4、w:2、x:1。
|
||||
|
||||
```html
|
||||
# chmod [-R] xyz dirname/filename
|
||||
```
|
||||
|
||||
范例:将 .bashrc 文件的权限修改为 -rwxr-xr--。
|
||||
|
||||
```html
|
||||
# chmod 754 .bashrc
|
||||
```
|
||||
|
||||
也可以使用符号来设定权限。
|
||||
|
||||
```html
|
||||
# chmod [ugoa] [+-=] [rwx] dirname/filename
|
||||
- u:拥有者
|
||||
- g:所属群组
|
||||
- o:其他人
|
||||
- a:所有人
|
||||
- +:添加权限
|
||||
- -:移除权限
|
||||
- =:设定权限
|
||||
```
|
||||
|
||||
范例:为 .bashrc 文件的所有用户添加写权限。
|
||||
|
||||
```html
|
||||
# chmod a+w .bashrc
|
||||
```
|
||||
|
||||
## 目录的权限
|
||||
|
||||
文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。
|
||||
|
||||
目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。
|
||||
|
||||
## 文件默认权限
|
||||
|
||||
文件默认权限:文件默认没有可执行权限,因此为 666 ,也就是 -rw-rw-rw- 。
|
||||
目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。
|
||||
|
||||
可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r-- 。
|
||||
|
||||
## 目录配置
|
||||
|
||||
为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:
|
||||
|
||||
- / (root, 根目录)
|
||||
- /usr (unix software resource):所有系统默认软件都会安装到这个目录;
|
||||
- /var (variable):存放系统或程序运行过程中的数据文件。
|
||||
|
||||
完整的目录树如下:
|
||||
|
||||

|
||||
|
||||
# 文件与目录
|
||||
|
||||
## 文件时间
|
||||
|
||||
1. modification time (mtime):文件的内容更新就会更新;
|
||||
2. status time (ctime):文件的状态(权限、属性)更新就会更新;
|
||||
3. access time (atime):读取文件时就会更新。
|
||||
|
||||
## 文件与目录的基本操作
|
||||
|
||||
### 1. ls
|
||||
|
||||
列出文件或者目录的信息,目录的信息就是其中包含的文件。
|
||||
|
||||
```html
|
||||
# ls [-aAdfFhilnrRSt] file|dir
|
||||
-a :列出全部的文件
|
||||
-d :仅列出目录本身
|
||||
-l :以长数据串行列出,包含文件的属性与权限等等数据
|
||||
```
|
||||
|
||||
### 2. cp
|
||||
|
||||
复制操作。
|
||||
|
||||
如果源文件有两个以上,则目的文件一定要是目录才行。
|
||||
|
||||
```html
|
||||
cp [-adfilprsu] source destination
|
||||
-a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明
|
||||
-d :若来源文件为链接文件,则复制链接文件属性而非文件本身
|
||||
-i :若目标文件已经存在时,在覆盖前会先询问
|
||||
-p :连同文件的属性一起复制过去
|
||||
-r :递归持续复制
|
||||
-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制
|
||||
--preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了
|
||||
```
|
||||
|
||||
### 3. rm
|
||||
|
||||
移除操作。
|
||||
|
||||
```html
|
||||
# rm [-fir] 文件或目录
|
||||
-r :递归删除
|
||||
```
|
||||
|
||||
### 4. mv
|
||||
|
||||
移动操作。
|
||||
|
||||
```html
|
||||
# mv [-fiu] source destination
|
||||
# mv [options] source1 source2 source3 .... directory
|
||||
-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
|
||||
```
|
||||
|
||||
## 获取文件内容
|
||||
|
||||
### 1. cat
|
||||
|
||||
取得文件内容。
|
||||
|
||||
```html
|
||||
# cat [-AbEnTv] filename
|
||||
-n :打印出行号, 连同空白行也会有行号,与 -b 的选项不同
|
||||
```
|
||||
|
||||
### 2. tac
|
||||
|
||||
是 cat 的反向操作,从最后一行开始打印。
|
||||
|
||||
### 3. more
|
||||
|
||||
可以一页一页查看文件内容,和文本编辑器类似。
|
||||
|
||||
### 4. less
|
||||
|
||||
和 more 类似。
|
||||
|
||||
### 5. head
|
||||
|
||||
可以取得文件前几行。
|
||||
|
||||
```html
|
||||
# head [-n number] filename
|
||||
-n :后面接数字,代表显示几行的意思
|
||||
```
|
||||
|
||||
### 6. tail
|
||||
|
||||
是 head 的反向操作,只是取得是后几行。
|
||||
|
||||
### 7. od
|
||||
|
||||
可以以字符或者十六进制的形式显示二进制文件。
|
||||
|
||||
### 8. touch
|
||||
|
||||
修改文件时间或者建立新文件。
|
||||
|
||||
```html
|
||||
# touch [-acdmt] filename
|
||||
-a : 更新 atime
|
||||
-c : 更新 ctime,若该文件不存在则不建立新文件
|
||||
-m : 更新 mtime
|
||||
-d : 后面可以接欲更新的日期而不用目前的日期,也可以使用 --date="日期或时间"
|
||||
-t :后面可以接欲更新的时间而不用目前的时间,格式为[YYYYMMDDhhmm]
|
||||
```
|
||||
|
||||
## 指令与文件搜索
|
||||
|
||||
### 1. which
|
||||
|
||||
指令搜索。
|
||||
|
||||
```html
|
||||
# which [-a] command
|
||||
-a :将所有指令列出,而不是只列第一个
|
||||
```
|
||||
|
||||
### 2. whereis
|
||||
|
||||
whereis 搜索文件的速度比较快,因为它只搜索几个特定的目录。
|
||||
|
||||
```html
|
||||
# whereis [-bmsu] dirname/filename
|
||||
```
|
||||
|
||||
### 3. locate
|
||||
|
||||
locate 可以用关键字或者正则表达式进行搜索。
|
||||
|
||||
locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。
|
||||
|
||||
```html
|
||||
# locate [-ir] keyword
|
||||
-r:接正则表达式
|
||||
```
|
||||
|
||||
### 4. find
|
||||
|
||||
find 可以使用文件的属性和权限进行搜索。
|
||||
|
||||
```html
|
||||
# find filename [option]
|
||||
```
|
||||
|
||||
#### 4.1 与时间有关的选项
|
||||
|
||||
```html
|
||||
-mtime n :列出在 n 天前的那一天修改过内容的文件
|
||||
-mtime +n :列出在 n 天之前(不含 n 天本身)修改过内容的文件
|
||||
-mtime -n :列出在 n 天之内(含 n 天本身)修改过内容的文件
|
||||
-newer file : 列出比 file 更新的文件
|
||||
```
|
||||
|
||||
+4、4 和 -4 的指示的时间范围如下:
|
||||
|
||||

|
||||
|
||||
#### 4.2 与文件拥有者和所属群组有关的选项
|
||||
|
||||
```html
|
||||
-uid n
|
||||
-gid n
|
||||
-user name
|
||||
-group name
|
||||
-nouser :搜索拥有者不存在 /etc/passwd 的文件
|
||||
-nogroup:搜索所属群组不存在于 /etc/group 的文件
|
||||
```
|
||||
|
||||
#### 4.3 与文件权限和名称有关的选项
|
||||
|
||||
```html
|
||||
-name filename
|
||||
-size [+-]SIZE:搜寻比 SIZE 还要大(+)或小(-)的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB还要大的文件,就是 -size +50k
|
||||
-type TYPE
|
||||
-perm mode :搜索权限等于 mode 的文件
|
||||
-perm -mode :搜索权限包含 mode 的文件
|
||||
-perm /mode :搜索权限包含任一 mode 的文件
|
||||
```
|
||||
|
||||
# 磁盘与文件系统
|
||||
|
||||
## 文件系统的组成
|
||||
|
||||
对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统,因此只有文件系统能被挂载,而分区不能被挂载。
|
||||
|
||||
文件系统有以下三个结构:
|
||||
|
||||
1. superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
|
||||
2. inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的数据所在的 block 号码;
|
||||
3. block:记录文件的内容,若文件太大时,会占用多个 block。
|
||||
|
||||
当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
|
||||
|
||||
磁盘碎片是指一个文件内容所在的 block 过于分散。
|
||||
|
||||
Ext2 文件系统使用了上述的文件结构,并在此之上加入了 block 群组的概念,也就是将一个文件系统划分为多个 block 群组,方便管理。
|
||||
|
||||

|
||||
|
||||
## inode
|
||||
|
||||
Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。
|
||||
|
||||
inode 中记录了文件内容所在的 block,但是每个 block 非常小,一个大文件随便都需要几十万的 block,而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用,使用用 block 来扩充大小,也就是让 block 来记录文件包含的 block。
|
||||
|
||||

|
||||
|
||||
inode 具体包含以下信息:
|
||||
|
||||
- 该文件的存取模式(read/write/excute);
|
||||
- 该文件的拥有者与群组(owner/group);
|
||||
- 该文件的容量;
|
||||
- 该文件建立或状态改变的时间(ctime);
|
||||
- 最近一次的读取时间(atime);
|
||||
- 最近修改的时间(mtime);
|
||||
- 定义文件特性的旗标(flag),如 SetUID...;
|
||||
- 该文件真正内容的指向 (pointer)。
|
||||
|
||||
## 目录的 inode 与 block
|
||||
|
||||
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名,可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
|
||||
|
||||
## 实体链接与符号链接
|
||||
|
||||
```html
|
||||
# ln [-sf] source_filename dist_filename
|
||||
-s :默认是 hard link,加 -s 为 symbolic link
|
||||
-f :如果目标5文件存在时,先删除目标文件
|
||||
```
|
||||
|
||||
### 1. 实体链接
|
||||
|
||||
hard link 只是在某个目录下新增一个条目,使得新增的条目链接到文件的 inode 上。删除任意一个条目,文件还是存在,只要引用数量不为 0。
|
||||
|
||||
有以下限制:不能跨越 Filesystem;不能对目录进行链接。
|
||||
|
||||
```html
|
||||
# ln /etc/crontab .
|
||||
# ll -i /etc/crontab crontab
|
||||
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab
|
||||
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
|
||||
```
|
||||
|
||||
### 2. 符号链接
|
||||
|
||||
symbolic link 可以理解为 Windows 的快捷方式,通过建立一个独立的文件,这个文件的数据的读取指向链接的那个文件。当源文件被删除了,链接文件就打不开了。
|
||||
|
||||
symbolic link 可以为目录建立链接。
|
||||
|
||||
```html
|
||||
# ll -i /etc/crontab /root/crontab2
|
||||
34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
|
||||
53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab
|
||||
```
|
||||
|
||||
# 压缩与打包
|
||||
|
||||
## 压缩
|
||||
|
||||
Linux 底下有很多压缩文件的扩展名,常见的如下:
|
||||
|
||||
| 扩展名 | 压缩程序 |
|
||||
| -- | -- |
|
||||
| \*.Z | compress |
|
||||
|\*.zip | zip |
|
||||
|\*.gz | gzip|
|
||||
|\*.bz2 | bzip2 |
|
||||
|\*.xz | xz |
|
||||
|\*.tar | tar 程序打包的数据,没有经过压缩 |
|
||||
|\*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 |
|
||||
|\*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 |
|
||||
|\*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 |
|
||||
|
||||
### 1. gzip
|
||||
|
||||
gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。
|
||||
|
||||
经过 gzip 压缩过,源文件就不存在了。
|
||||
|
||||
有 9 个不同的压缩等级可以使用。
|
||||
|
||||
可以使用 zcat、zmore、zless 来读取压缩文件的内容。
|
||||
|
||||
```html
|
||||
$ gzip [-cdtv#] filename
|
||||
-c :将压缩的数据输出到屏幕上
|
||||
-d :解压缩
|
||||
-t :检验压缩文件是否出错
|
||||
-v :显示压缩比等信息
|
||||
-# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为6
|
||||
```
|
||||
|
||||
### 2. bzip2
|
||||
|
||||
提供比 gzip 更高的压缩比。
|
||||
|
||||
查看命令:bzcat、bzmore、bzless、bzgrep。
|
||||
|
||||
```html
|
||||
$ bzip2 [-cdkzv#] filename
|
||||
-k :保留源文件
|
||||
```
|
||||
|
||||
### 3. xz
|
||||
|
||||
提供比 bzip2 更佳的压缩比。
|
||||
|
||||
可以看到,gzip、bzip2、xz 的压缩比不断优化,不过要注意,压缩比越高,压缩的时间也越长。
|
||||
|
||||
查看命令:xzcat、xzmore、xzless、xzgrep。
|
||||
|
||||
```html
|
||||
$ xz [-dtlkc#] filename
|
||||
```
|
||||
|
||||
## 打包
|
||||
|
||||
压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。
|
||||
|
||||
```html
|
||||
$ tar [-z|-j|-J] [cv] [-f 新建的tar文件] filename... ==打包压缩
|
||||
$ tar [-z|-j|-J] [tv] [-f 已有的tar文件] ==查看
|
||||
$ tar [-z|-j|-J] [xv] [-f 已有的tar文件] [-C 目录] ==解压缩
|
||||
-z :使用zip;
|
||||
-j :使用bzip2;
|
||||
-J :使用xz;
|
||||
-c :新建打包文件;
|
||||
-t :查看打包文件里面有哪些文件;
|
||||
-x :解打包或解压缩的功能;
|
||||
-v :在压缩/解压缩的过程中,显示正在处理的文件名;
|
||||
-f : filename:要处理的文件;
|
||||
-C 目录 : 在特定目录解压缩。
|
||||
```
|
||||
|
||||
最常用的方式如下:
|
||||
|
||||
打包压缩 : tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称
|
||||
查 看 : tar -jtv -f filename.tar.bz2
|
||||
解压缩 :tar -jxv -f filename.tar.bz2 -C 要解压缩的目录
|
||||
|
||||
# BASH
|
||||
|
||||
可以通过 shell 请求内核提供服务,Bash 正是 shell 的一种。
|
||||
|
||||
## Bash特性
|
||||
|
||||
**1. 命令历史**
|
||||
|
||||
记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中, ~/.bash_history 文件中记录的是前一次登录所执行过的命令。
|
||||
|
||||
**2. 命令与文件补全**
|
||||
|
||||
快捷键:tab
|
||||
|
||||
**3. 命名别名**
|
||||
|
||||
例如 lm 是 ls -al 的别名。
|
||||
|
||||
**4. shell scripts**
|
||||
|
||||
**5. 通配符**
|
||||
|
||||
例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。
|
||||
|
||||
## 变量操作
|
||||
|
||||
对一个变量赋值直接使用 = ,对变量取用需要在变量前加上 \$ ,也可以用 \${} 的形式,输出变量使用 echo 命令。
|
||||
|
||||
```bash
|
||||
$ var=abc
|
||||
$ echo $var
|
||||
$ echo ${var}
|
||||
```
|
||||
|
||||
变量内容如果有空格,需要使用双引号或者单引号。双引号内的特殊字符可以保留原本特性,例如var="lang is \$LANG",则var的值为 lang is zh_TW.UTF-8;而单引号内的特殊字符就是特殊字符本身,例如 var='lang is \$LANG',则 var 的值为 lang is \$LANG。
|
||||
|
||||
|
||||
可以使用 \`指令\` 或者 \$(指令) 的方式将指令的执行结果赋值给变量。例如 version=\$(uname -r),则 version 的值为 3.10.0-229.el7.x86_64。
|
||||
|
||||
可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。
|
||||
|
||||
Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:
|
||||
|
||||
```html
|
||||
$ declare [-aixr] variable
|
||||
-a : 定义为数组类型
|
||||
-i : 定义为整数类型
|
||||
-x : 定义为环境变量
|
||||
-r : 定义为readonly类型
|
||||
```
|
||||
|
||||
使用 [ ] 来对数组进行操作:
|
||||
|
||||
```bash
|
||||
$ array[1]=a
|
||||
$ array[2]=b
|
||||
$ echo ${array[1]}
|
||||
```
|
||||
|
||||
## 指令搜索顺序
|
||||
|
||||
1. 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ;
|
||||
2. 由别名找到该指令来执行;
|
||||
3. 由 Bash 内建的指令来执行;
|
||||
4. 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。
|
||||
|
||||
## 数据流重定向
|
||||
|
||||
重定向就是使用文件代替标准输入、标准输出和标准错误输出。
|
||||
|
||||
1. 标准输入 (stdin) :代码为 0 ,使用 < 或 << ;
|
||||
2. 标准输出 (stdout) :代码为 1 ,使用 > 或 >> ;
|
||||
3. 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>> ;
|
||||
|
||||
其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。
|
||||
|
||||
可以将不需要的标准输出以及标准错误输出重定向到 /dev/null ,相当于扔进垃圾箱。
|
||||
|
||||
如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。
|
||||
|
||||
```bash
|
||||
$ find /home -name .bashrc > list 2>&1
|
||||
```
|
||||
|
||||
## 管线指令
|
||||
|
||||
管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的格式时就可以使用管线。在命令之间使用 | 分隔各个管线命令。
|
||||
|
||||
```bash
|
||||
$ ls -al /etc | less
|
||||
```
|
||||
|
||||
### 1. 提取指令:cut
|
||||
|
||||
提取过程一行一行地进行。
|
||||
|
||||
cut 对数据进行切分,取出想要的部分。
|
||||
|
||||
```html
|
||||
$ cut
|
||||
-d :分隔符
|
||||
-f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
|
||||
-c :以字符为单位取出区间
|
||||
```
|
||||
|
||||
范例1:last 将显示的登入者的信息,要求仅显示用户名。
|
||||
|
||||
```html
|
||||
$ last
|
||||
root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
|
||||
root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
|
||||
root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
|
||||
|
||||
$ last | cut -d ' ' -f 1
|
||||
```
|
||||
|
||||
范例2:将 export 输出的讯息,取得第 12 字符以后的所有字符串。
|
||||
|
||||
```html
|
||||
$ export
|
||||
declare -x HISTCONTROL="ignoredups"
|
||||
declare -x HISTSIZE="1000"
|
||||
declare -x HOME="/home/dmtsai"
|
||||
declare -x HOSTNAME="study.centos.vbird"
|
||||
.....(其他省略).....
|
||||
|
||||
$ export | cut -c 12
|
||||
```
|
||||
|
||||
### 2. 排序命令:sort、uniq
|
||||
|
||||
**sort** 进行排序。
|
||||
|
||||
```html
|
||||
$ sort [-fbMnrtuk] [file or stdin]
|
||||
-f :忽略大小写
|
||||
-b :忽略最前面的空格
|
||||
-M :以月份的名字来排序,例如 JAN, DEC
|
||||
-n :使用数字
|
||||
-r :反向排序
|
||||
-u :相当于 unique ,重复内容只出现一次
|
||||
-t :分隔符,默认为tab
|
||||
-k :指定排序的区间
|
||||
```
|
||||
|
||||
范例:/etc/passwd 内容是以 : 来分隔的,以第三栏来排序。
|
||||
|
||||
```html
|
||||
$ cat /etc/passwd | sort -t ':' -k 3
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash
|
||||
alex:x:1001:1002::/home/alex:/bin/bash
|
||||
arod:x:1002:1003::/home/arod:/bin/bash
|
||||
```
|
||||
|
||||
**uniq** 可以将重复的数据只取一个。
|
||||
|
||||
```html
|
||||
$ uniq [-ic]
|
||||
-i :忽略大小写
|
||||
-c :进行计数
|
||||
```
|
||||
|
||||
范例:取得每个人的登录总次数
|
||||
|
||||
```html
|
||||
$ last | cut -d ' ' -f 1 | sort | uniq -c
|
||||
1
|
||||
6 (unknown
|
||||
47 dmtsai
|
||||
4 reboot
|
||||
7 root
|
||||
1 wtmp
|
||||
```
|
||||
|
||||
### 3. 双向输出重定向:tee
|
||||
|
||||
输出重定向会将输出内容重定向到文件中,而 **tee** 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。
|
||||
|
||||
```html
|
||||
$ tee [-a] file
|
||||
```
|
||||
|
||||
### 4. 字符转换指令:tr、col、expand、join、paste
|
||||
|
||||
**tr** 用来删除一行中的字符,或者对字符进行替换。
|
||||
|
||||
```html
|
||||
$ tr [-ds] SET1 ...
|
||||
-d : 删除行中 SET1 这个字符串
|
||||
```
|
||||
|
||||
范例,将 last 输出的信息所有小写转换为大写。
|
||||
|
||||
```html
|
||||
$ last | tr '[a-z]' '[A-Z]'
|
||||
```
|
||||
|
||||
**col** 将 tab 字符转为空格字符。
|
||||
|
||||
```html
|
||||
$ col [-xb]
|
||||
-x : 将 tab 键转换成对等的空格键
|
||||
```
|
||||
|
||||
**expand** 将 tab 转换一定数量的空格,默认是 8 个。
|
||||
|
||||
```html
|
||||
$ expand [-t] file
|
||||
-t :tab 转为空格的数量
|
||||
```
|
||||
|
||||
**join** 将有相同数据的那一行合并在一起。
|
||||
|
||||
```html
|
||||
$ join [-ti12] file1 file2
|
||||
-t :分隔符,默认为空格
|
||||
-i :忽略大小写的差异
|
||||
-1 :第一个文件所用的比较字段
|
||||
-2 :第二个文件所用的比较字段
|
||||
```
|
||||
|
||||
**paste** 直接将两行粘贴在一起。
|
||||
|
||||
```html
|
||||
$ paste [-d] file1 file2
|
||||
-d :分隔符,默认为 tab
|
||||
```
|
||||
|
||||
### 5. 分区指令:split
|
||||
|
||||
**split** 将一个文件划分成多个文件。
|
||||
|
||||
```html
|
||||
$ split [-bl] file PREFIX
|
||||
-b :以大小来进行分区,可加单位,例如 b, k, m 等
|
||||
-l :以行数来进行分区。
|
||||
- PREFIX :分区文件的前导名称
|
||||
```
|
||||
|
||||
# 正规表示法与文件格式化处理
|
||||
|
||||
## grep
|
||||
|
||||
使用正则表示式把匹配的行提取出来。
|
||||
|
||||
```html
|
||||
$ grep [-acinv] [--color=auto] 搜寻字符串 filename
|
||||
-a : 将 binary 文件以 text 文件的方式进行搜寻
|
||||
-c : 计算找到个数
|
||||
-i : 忽略大小写
|
||||
-n : 输出行号
|
||||
-v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行
|
||||
--color=auto :找到的关键字加颜色显示
|
||||
```
|
||||
|
||||
范例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)
|
||||
|
||||
```html
|
||||
$ grep -n 'the' regular_express.txt
|
||||
8:I can't finish the test.
|
||||
12:the symbol '*' is represented as start.
|
||||
15:You are the best is mean you are the no. 1.
|
||||
16:The world Happy is the same with "glad".
|
||||
18:google is the best tools for search keyword
|
||||
```
|
||||
|
||||
因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用使用转义字符进行转义。
|
||||
|
||||
```html
|
||||
$ grep -n 'go\{2,5\}g' regular_express.txt
|
||||
```
|
||||
|
||||
其它正则表达式请参考 [正则表达式](https://github.com/00000H/notes/blob/master/notes/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F.md).
|
||||
|
||||
## printf
|
||||
|
||||
用于格式化输出。
|
||||
|
||||
它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
|
||||
|
||||
```html
|
||||
$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
|
||||
DmTsai 80 60 92 77.33
|
||||
VBird 75 55 80 70.00
|
||||
Ken 60 90 70 73.33
|
||||
```
|
||||
|
||||
## awk
|
||||
|
||||
```html
|
||||
$ awk '条件类型 1{动作 1} 条件类型 2{动作 2} ...' filename
|
||||
```
|
||||
|
||||
awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。
|
||||
|
||||
范例 1:取出登录用户的用户名和 ip
|
||||
|
||||
```html
|
||||
$ last -n 5
|
||||
dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in
|
||||
dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
|
||||
dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
|
||||
dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
|
||||
dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
|
||||
|
||||
$ last -n 5 | awk '{print $1 "\t" $3}
|
||||
```
|
||||
|
||||
awk 变量:
|
||||
|
||||
| 变量名称 | 代表意义 |
|
||||
| -- | -- |
|
||||
| NF | 每一行拥有的字段总数 |
|
||||
| NR | 目前所处理的是第几行数据 |
|
||||
| FS | 目前的分隔字符,默认是空格键 |
|
||||
|
||||
范例 2:输出正在处理的行号,并显示每一行有多少字段
|
||||
|
||||
```html
|
||||
$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
|
||||
dmtsai lines: 1 columns: 10
|
||||
dmtsai lines: 2 columns: 10
|
||||
dmtsai lines: 3 columns: 10
|
||||
dmtsai lines: 4 columns: 10
|
||||
dmtsai lines: 5 columns: 9
|
||||
```
|
||||
|
||||
可以使用大于等于逻辑,其中等于使用 ==。
|
||||
|
||||
范例 3:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。
|
||||
|
||||
```text
|
||||
cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
|
||||
root 0
|
||||
bin 1
|
||||
daemon 2
|
||||
```
|
||||
|
||||
# vim 三个模式
|
||||
|
||||

|
||||
|
||||
在指令列模式下,有以下命令用于离开或者存储文件。
|
||||
|
||||
| 命令 | 作用 |
|
||||
| -- | -- |
|
||||
| :w | 写入磁盘|
|
||||
| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
|
||||
| :q | 离开|
|
||||
| :q! | 强制离开不保存|
|
||||
| :wq | 写入磁盘后离开|
|
||||
| :wq!| 强制写入磁盘后离开|
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.
|
||||
- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html)
|
||||
|
@ -1,176 +0,0 @@
|
||||
# 直观理解
|
||||
|
||||
统计一堆牌中有多少张黑桃,最简单的方法是一张一张去数。而 MapReduce 的方法是:
|
||||
|
||||
1. 把这堆牌分配给多个玩家;
|
||||
2. 让每个玩家数自己手中有多少张黑桃;
|
||||
3. 把所有玩家数出的黑桃数加起来就是最后的结果。
|
||||
|
||||
通过让多个玩家来并行地进行统计,MapReduce 可以将统计时间大大缩短。
|
||||
|
||||
# 大数据
|
||||
|
||||
从大量数据中快速提取出有价值的信息。注意到这里的数据不一定需要同种类型的数据,可以是多种多样的数据,包括非结构化数据、半结构化数据以及结构化数据。
|
||||
|
||||
# 基本原理
|
||||
|
||||
MapReduce 用来对大数据中的海量数据进行离线分析。
|
||||
|
||||
MapReduce 分为两个阶段:Map 阶段和 Reduce 阶段。
|
||||
|
||||
在下图中,file 包含多个 block,每个块代表一个海量数据。file 被划分为多个 split,每个分片由一个 Mapper Task 去处理。Map 过程中输入的是 [K1, V1] 数据,这种是一种键值对形式的数据,键为泛型 K1 的类型,值为泛型 V1 的类型。输入也是一个泛型的键值对 [K2, V2]。Shuffle 过程主要是对 Map 阶段输出的键值对进行整合,将键相同的键值对整合到一组中,得到 [K2, {V2,...}] 的整合数据,作为 Reudce 阶段的输入。Reduce 处理完之后得到 [K3, V3] 键值对,Hadoop 会将输出的数据存到分布式文件系统 HDFS 中。
|
||||
|
||||

|
||||
|
||||
# WordCount 实战
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
创建 Maven 项目,在 pom.xml 中添加以下依赖:
|
||||
|
||||
```
|
||||
<properties>
|
||||
<hadoopVersion>3.0.0</hadoopVersion>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-common</artifactId>
|
||||
<version>${hadoopVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-hdfs</artifactId>
|
||||
<version>${hadoopVersion}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-mapreduce-client-core</artifactId>
|
||||
<version>${hadoopVersion}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-client</artifactId>
|
||||
<version>${hadoopVersion}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## 文本
|
||||
|
||||
```html
|
||||
the weather is good
|
||||
today is good
|
||||
good weather is good
|
||||
today has good weather
|
||||
```
|
||||
|
||||
将文本的每一行分成一片,每一片的数据提供给 Mapper 进行处理,因此需要 4 个 Mapper。其中 K1 为行号,V1 位该行的字符串。
|
||||
|
||||
```html
|
||||
Split-0: [0, "the weather is good]
|
||||
Split-1: [1, "today is good"]
|
||||
Split-2: [2, "good weather is good"]
|
||||
Split-3: [3, "today has good weather]
|
||||
```
|
||||
|
||||
## Mapper
|
||||
|
||||
Mapper 类需要提供四个泛型,分别为 K1, V1, K2, V2。
|
||||
|
||||
```java
|
||||
class WordCountMappper extends Mapper<LongWritable, Text, Text, IntWritable> {
|
||||
@Override
|
||||
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
|
||||
String line = value.toString();
|
||||
String[] words = line.split(" ");
|
||||
for (String word : words) {
|
||||
context.write(new Text(word), new IntWritable(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
Mapper-0: ["the", 1], ["weather", 1], ["is", 1], ["good", 1]
|
||||
Mapper-1: ["today", 1], ["is", 1], ["good", 1]
|
||||
Mapper-2: ["good", 2], ["weather", 1], ["is", 1]
|
||||
Mapper-3: [today", 1], ["has", 1], ["good", 1], ["weater", 1]
|
||||
```
|
||||
|
||||
## Shuffle
|
||||
|
||||
排序并将拥有相同键的数据整合。
|
||||
|
||||
```html
|
||||
["good", {1, 1, 2, 1}]
|
||||
["has", {1}]
|
||||
["is", {1, 1, 1}]
|
||||
["the", {1}]
|
||||
["today", {1, 2}]
|
||||
["weater", {1,1}]
|
||||
```
|
||||
|
||||
## Reducer
|
||||
|
||||
Reducer 同样要指定 4 个泛型:K2, V2, K3, V3
|
||||
|
||||
```java
|
||||
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
|
||||
@Override
|
||||
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
|
||||
Integer count = 0;
|
||||
for (IntWritable value : values) {
|
||||
count += value.get();
|
||||
}
|
||||
context.write(key, new IntWritable(count));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
Reducer-0: ["good", 5]
|
||||
Reducer-1: ["has", 1]
|
||||
Reducer-2: ["is", 3]
|
||||
Reducer-3: ["the", 1]
|
||||
Reducer-4: ["today", 2]
|
||||
Reducer-5: ["weather", 3]
|
||||
```
|
||||
|
||||
## Job
|
||||
|
||||
将项目打包成 jar 包后使用 Hadoop 来运行。
|
||||
|
||||
```java
|
||||
public class WordCountMapReduce {
|
||||
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
|
||||
Configuration conf = new Configuration();
|
||||
// 新建 Job
|
||||
Job job = Job.getInstance(conf, "wordcount");
|
||||
job.setJarByClass(WordCountMapReduce.class);
|
||||
job.setMapperClass(WordCountMappper.class);
|
||||
job.setReducerClass(WordCountReducer.class);
|
||||
// 设置 Mapper 和 Reducer 的 Key Value 类型
|
||||
job.setMapOutputKeyClass(Text.class);
|
||||
job.setOutputValueClass(IntWritable.class);
|
||||
job.setOutputKeyClass(Text.class);
|
||||
job.setOutputValueClass(IntWritable.class);
|
||||
// 设置 HDFS 路径
|
||||
FileInputFormat.setInputPaths(job, new Path("hdfs://domain:8020/words"));
|
||||
FileOutputFormat.setOutputPath(job, new Path("hdfs://domain:8020/words"));
|
||||
// 运行
|
||||
boolean b = job.waitForCompletion(true);
|
||||
if (!b) {
|
||||
System.err.println("This task has failed!!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [Hadoop MapReduce 入门](http://www.jikexueyuan.com/course/2686.html)
|
||||
|
@ -1,28 +0,0 @@
|
||||
# 主从复制与读写分离
|
||||
|
||||
## 1. 主从复制
|
||||
|
||||

|
||||
|
||||
主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。
|
||||
|
||||
1. **binlog 线程**:负责将主服务器上的数据更改写入二进制文件(binlog)中。
|
||||
2. **I/O 线程**:负责从主服务器上读取二进制日志文件,并写入中继日志中。
|
||||
3. **SQL 线程**:负责读取中继日志并重放其中的 SQL 语句。
|
||||
|
||||
## 2. 读写分离
|
||||
|
||||

|
||||
|
||||
主服务器用来处理写操作,而从服务器用来处理读操作。
|
||||
|
||||
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器,例如 Amoeba 代理服务器。
|
||||
|
||||
MySQL 读写分离能提高性能的原因在于:
|
||||
|
||||
1. 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
|
||||
2. 从服务器可以配置 MyISAM 引擎,提升查询技能以及节约系统开销;
|
||||
3. 增加冗余,提高可用性。
|
||||
|
||||
- [MySQL 读写分离介绍及搭建 ](https://segmentfault.com/a/1190000003716617)
|
||||
- [Mysql 分表和分区的区别、分库分表介绍与区别 ](http://www.cnblogs.com/langtianya/p/4997768.html)
|
@ -1,382 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 存储引擎
|
||||
|
||||
## 1. InnoDB
|
||||
|
||||
InnoDB 是 MySQL 的默认事务型引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
|
||||
|
||||
采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。
|
||||
|
||||
表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。
|
||||
|
||||
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建 hash 索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
|
||||
|
||||
通过一些机制和工具支持真正的热备份。
|
||||
|
||||
## 2. MyISAM
|
||||
|
||||
MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且奔溃后无法安全恢复。
|
||||
|
||||
只能对整张表加锁,而不是针对行。
|
||||
|
||||
可以手工或者自动执行检查和修复操作,但是和事务恢复以及奔溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
|
||||
|
||||
可以包含动态或者静态的行。
|
||||
|
||||
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机奔溃时会造成索引损坏,需要执行修复操作。
|
||||
|
||||
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。
|
||||
|
||||
对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
|
||||
|
||||
MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。
|
||||
|
||||
## 3. InnoDB 与 MyISAM 的比较
|
||||
|
||||
**事务**
|
||||
|
||||
InnoDB 是事务型的。
|
||||
|
||||
**备份**
|
||||
|
||||
InnoDB 支持在线热备份。
|
||||
|
||||
**奔溃恢复**
|
||||
|
||||
MyISAM 奔溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
|
||||
|
||||
**并发**
|
||||
|
||||
MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
|
||||
|
||||
**其它特性**
|
||||
|
||||
MyISAM 支持全文索引,地理空间索引;
|
||||
|
||||
# 数据类型
|
||||
|
||||
## 1. 整型
|
||||
|
||||
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 64 位存储空间,一般情况下越小的列越好。
|
||||
|
||||
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
|
||||
|
||||
## 2. 浮点数
|
||||
|
||||
FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
|
||||
|
||||
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
|
||||
|
||||
## 3. 字符串
|
||||
|
||||
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
|
||||
|
||||
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作,MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
|
||||
|
||||
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
|
||||
|
||||
## 4. 时间和日期
|
||||
|
||||
MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。
|
||||
|
||||
**DATATIME**
|
||||
|
||||
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
|
||||
|
||||
它与时区无关。
|
||||
|
||||
默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01016 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
|
||||
|
||||
**TIMESTAMP**
|
||||
|
||||
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
|
||||
|
||||
它和时区有关。
|
||||
|
||||
MySQL 提供了 FROM_UNIXTIME() 函数把 Unxi 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 Unix 时间戳。
|
||||
|
||||
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
|
||||
|
||||
应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
|
||||
|
||||
# 索引
|
||||
|
||||
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
|
||||
|
||||
索引能够轻易将查询性能提升几个数量级。
|
||||
|
||||
对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
|
||||
|
||||
## 1. 索引分类
|
||||
|
||||
### 1.1 B-Tree 索引
|
||||
|
||||
B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
|
||||
|
||||
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。
|
||||
|
||||
可以指定多个列作为索引列,多个索引列共同组成键。B-Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
|
||||
|
||||
除了用于查找,还可以用于排序和分组。
|
||||
|
||||
如果不是按照索引列的顺序进行查找,则无法使用索引。
|
||||
|
||||
### 1.2 哈希索引
|
||||
|
||||
基于哈希表实现,优点是查找非常快。
|
||||
|
||||
在 MySQL 中只有 Memory 引擎显式支持哈希索引。
|
||||
|
||||
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B-Tree 索引之上再创建一个哈希索引,这样就让 B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
|
||||
|
||||
限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。
|
||||
|
||||
### 1.3. 空间索引数据(R-Tree)
|
||||
|
||||
MyISAM 存储引擎支持空间索引,可以用于地理数据存储。
|
||||
|
||||
空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
|
||||
|
||||
### 1.4 全文索引
|
||||
|
||||
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。
|
||||
|
||||
使用 MATCH AGAINST,而不是普通的 WHERE。
|
||||
|
||||
## 2. 索引的优点
|
||||
|
||||
- 大大减少了服务器需要扫描的数据量;
|
||||
|
||||
- 帮助服务器避免进行排序和创建临时表;
|
||||
|
||||
- 将随机 I/O 变为顺序 I/O。
|
||||
|
||||
## 3. 索引优化
|
||||
|
||||
### 3.1 独立的列
|
||||
|
||||
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
|
||||
|
||||
例如下面的查询不能使用 actor_id 列的索引:
|
||||
|
||||
```sql
|
||||
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
|
||||
```
|
||||
|
||||
### 3.2 前缀索引
|
||||
|
||||
对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
|
||||
|
||||
对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1 ,此时每个记录都有唯一的索引与其对应。
|
||||
|
||||
### 3.3 多列索引
|
||||
|
||||
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 file_id 设置为多列索引。
|
||||
|
||||
```sql
|
||||
SELECT file_id, actor_ id FROM sakila.film_actor
|
||||
WhERE actor_id = 1 OR film_id = 1;
|
||||
```
|
||||
|
||||
### 3.4 索引列的顺序
|
||||
|
||||
让选择性最强的索引列放在前面,例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
|
||||
|
||||
```sql
|
||||
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
|
||||
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
|
||||
COUNT(*)
|
||||
FROM payment;
|
||||
```
|
||||
|
||||
```html
|
||||
staff_id_selectivity: 0.0001
|
||||
customer_id_selectivity: 0.0373
|
||||
COUNT(*): 16049
|
||||
```
|
||||
|
||||
### 3.5 聚簇索引
|
||||
|
||||

|
||||
|
||||
聚簇索引并不是一种索引类型,而是一种数据存储方式。
|
||||
|
||||
术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引的数据行存放在 B-Tree 的叶子页中。
|
||||
|
||||
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
|
||||
|
||||
**优点**
|
||||
|
||||
1. 可以把相关数据保存在一起,减少 I/O 操作;
|
||||
2. 因为数据保存在 B-Tree 中,因此数据访问更快。
|
||||
|
||||
**缺点**
|
||||
|
||||
1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。
|
||||
2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
|
||||
3. 更新操作代价很高,因为每个被更新的行都会移动到新的位置。
|
||||
4. 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。
|
||||
5. 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。
|
||||
|
||||
### 3.6 覆盖索引
|
||||
|
||||
索引包含所有需要查询的字段的值。
|
||||
|
||||
## 4. B-Tree 和 B+Tree 原理
|
||||
|
||||
### 4. 1 B-Tree
|
||||
|
||||

|
||||
|
||||
为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data],key 为记录的键,data 为数据记录除 key 外的数据。
|
||||
|
||||
B-Tree 是满足下列条件的数据结构:
|
||||
|
||||
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
|
||||
- 一个节点中的 key 从左到右非递减排列;
|
||||
- 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null,则该指针指向节点的所有 key 大于 key<sub>i</sub> 且小于 key<sub>i+1</sub>。
|
||||
|
||||
在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到 null 指针,前者查找成功,后者查找失败。
|
||||
|
||||
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
|
||||
|
||||
### 4.2 B+Tree
|
||||
|
||||

|
||||
|
||||
与 B-Tree 相比,B+Tree 有以下不同点:
|
||||
|
||||
- 每个节点的指针上限为 2d 而不是 2d+1;
|
||||
- 内节点不存储 data,只存储 key,叶子节点不存储指针。
|
||||
|
||||
### 4.3 带有顺序访问指针的 B+Tree
|
||||
|
||||

|
||||
|
||||
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
|
||||
|
||||
### 4.4 为什么使用 B-Tree 和 B+Tree
|
||||
|
||||
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。
|
||||
|
||||
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为 4k),主存和磁盘以页为单位交换数据。
|
||||
|
||||
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。
|
||||
|
||||
B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。
|
||||
|
||||
# 查询性能优化
|
||||
|
||||
## 1. Explain
|
||||
|
||||
用来分析 SQL 语句,分析结果中比较重要的字段有:
|
||||
|
||||
- select_type : 查询类型,有简单查询、联合查询和子查询
|
||||
|
||||
- key : 使用的索引
|
||||
|
||||
- rows : 扫描的行数
|
||||
|
||||
## 2. 减少返回的列
|
||||
|
||||
慢查询主要是因为访问了过多数据,除了访问过多行之外,也包括访问过多列。
|
||||
|
||||
最好不要使用 SELECT * 语句,要根据需要选择查询的列。
|
||||
|
||||
## 3. 减少返回的行
|
||||
|
||||
最好使用 LIMIT 语句来取出想要的那些行。
|
||||
|
||||
还可以建立索引来减少条件语句的全表扫描。例如对于下面的语句,不适用索引的情况下需要进行全表扫描,而使用索引只需要扫描几行记录即可,使用 Explain 语句可以通过观察 rows 字段来看出这种差异。
|
||||
|
||||
```sql
|
||||
SELECT * FROM sakila.film_actor WHERE film_id = 1;
|
||||
```
|
||||
|
||||
## 4. 拆分大的 DELETE 或 INSERT 语句
|
||||
|
||||
如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
|
||||
|
||||
```sql
|
||||
DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
|
||||
```
|
||||
```sql
|
||||
rows_affected = 0
|
||||
do {
|
||||
rows_affected = do_query(
|
||||
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
|
||||
} while rows_affected > 0
|
||||
```
|
||||
|
||||
# 分库与分表
|
||||
|
||||
**1. 分表与分区的不同**
|
||||
|
||||
分表,就是讲一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。
|
||||
|
||||
**2. 使用分库与分表的原因**
|
||||
|
||||
随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。
|
||||
|
||||
**3. 垂直切分**
|
||||
|
||||
将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。
|
||||
|
||||
**4. 水平切分**
|
||||
|
||||
把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分,
|
||||
|
||||
**5. 垂直切分与水平切分的选择**
|
||||
|
||||
如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。
|
||||
|
||||
如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。
|
||||
|
||||
**6. 水平切分的实现方式**
|
||||
|
||||
最简单的是使用 merge 存储引擎。
|
||||
|
||||
**7. 分库与分表存在的问题**
|
||||
|
||||
(1) 事务问题
|
||||
|
||||
在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
|
||||
|
||||
(2) 跨库跨表连接问题
|
||||
|
||||
在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。
|
||||
|
||||
# 故障转移和故障恢复
|
||||
|
||||
故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。
|
||||
|
||||
## 1. 故障转移
|
||||
|
||||
**1.1 提升备库或切换角色**
|
||||
|
||||
提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。
|
||||
|
||||
**1.2 虚拟 IP 地址和 IP 托管**
|
||||
|
||||
为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。
|
||||
|
||||
**1.3 中间件解决方案**
|
||||
|
||||
通过代理,可以路由流量到可以使用的服务器上。
|
||||
|
||||

|
||||
|
||||
**1.4 在应用中处理故障转移**
|
||||
|
||||
将故障转移整合到应用中可能导致应用变得太过笨拙。
|
||||
|
||||
## 2. 故障恢复
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 高性能 MySQL
|
||||
- [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
|
||||
- [MySQL 索引优化全攻略 ](http://www.runoob.com/w3cnote/mysql-index.html)
|
||||
- [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
|
@ -1 +0,0 @@
|
||||
|
@ -1,548 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第 1 章 作为命令行脚本
|
||||
|
||||
第一行注释告诉 Linux/Unix 系统当执行程序的时候,它应该使用哪个解释器。
|
||||
|
||||
```python
|
||||
#!/usr/bin/python
|
||||
# Filename : helloworld.py
|
||||
print 'Hello World'
|
||||
```
|
||||
|
||||
需要先给脚本设置可执行的权限。
|
||||
|
||||
```html
|
||||
$ chmod a+x helloworld.py
|
||||
$ ./helloworld.py
|
||||
Hello World
|
||||
```
|
||||
|
||||
通过以下命令可以把一个文件夹下的脚本都添加到 PATH 目录下,则可以在任何地方运行该脚本。
|
||||
|
||||
```html
|
||||
PATH=$PATH:/home/username/dir
|
||||
```
|
||||
|
||||
print 函数会自动加换行符,可以在 print 之后加逗号 , 来取消换行。
|
||||
|
||||
# 第 2 章 基本概念
|
||||
|
||||
## 2.1 复数
|
||||
|
||||
(2.3-4.6j)
|
||||
|
||||
## 2.2 字符串
|
||||
|
||||
单引号和双引号表示的字符串完全相同,三引号表示的字符串中间可以有单引号和双引号,并且可以换行。
|
||||
|
||||
```html
|
||||
'''This is the first line.
|
||||
This is the second line.
|
||||
"What's your name?," I asked.
|
||||
He said "Bond, James Bond."
|
||||
'''
|
||||
```
|
||||
|
||||
字符串行末的反斜杠表示下一行继续。
|
||||
|
||||
```html
|
||||
"This is the first sentence.\
|
||||
This is the second sentence."
|
||||
```
|
||||
|
||||
自然字符串用 r 作为前缀,当不想让字符串中的内容转义时使用,比如正则表达式。
|
||||
|
||||
```html
|
||||
r"Newlines are indicated by \n"
|
||||
```
|
||||
|
||||
Unicode 是书写国际文本的标准方法。在处理文本文件的时候应该使用 Unicode 字符串,特别是这个文件含有用非英语的语言写的文本时使用。
|
||||
|
||||
```html
|
||||
u"This is a Unicode string."
|
||||
```
|
||||
|
||||
## 2.3 对象
|
||||
|
||||
Python 中用到的任何东西都被称为对象;
|
||||
|
||||
## 2.4 逻辑行与物理行
|
||||
|
||||
可以将多个逻辑行写在一个物理行中,需要用分号分割;可以将一个逻辑行写在多个物理行中,需要在行末加 \ 。
|
||||
|
||||
## 2.5 缩进
|
||||
|
||||
同一层次的语句必须拥有相同的缩进。
|
||||
|
||||
# 第 3 章 运算符
|
||||
|
||||
| 运算符 | 说明 |
|
||||
| --- | --- |
|
||||
| ** | 幂 |
|
||||
| // | 取整除 |
|
||||
| % | 取模,不取整 |
|
||||
| ~ | 按位翻转,-(x+1) |
|
||||
| and | 与 |
|
||||
| or | 或 |
|
||||
| not | 非 |
|
||||
|
||||
|
||||
# 第 4 章 控制流
|
||||
|
||||
## 4.1 条件语句
|
||||
|
||||
Java 中的 else if 在 Python 中对应的语句为:elif。
|
||||
|
||||
True False
|
||||
|
||||
## 4.2 控制台输入
|
||||
|
||||
从控制台中输入一行:
|
||||
|
||||
```
|
||||
guess = int(raw_input('Enter an integer : '))
|
||||
```
|
||||
|
||||
## 4.3 循环
|
||||
|
||||
range(m, n, step),返回一个从 m 到 n,步长为 stap 的序列,不包含 n 。
|
||||
|
||||
for 后面的 else 语句在 break 退出的情况下不会执行:
|
||||
```
|
||||
for i in range(1, 5):
|
||||
print i
|
||||
else:
|
||||
print 'The for loop is over
|
||||
```
|
||||
|
||||
len(str) 返回字符串的长度;
|
||||
|
||||
# 第 5 章 函数
|
||||
|
||||
```
|
||||
def sayHello():
|
||||
print 'hello world!'
|
||||
|
||||
sayHello()
|
||||
```
|
||||
|
||||
global 用来声明全局变量。
|
||||
|
||||
## 5.1 默认参数
|
||||
|
||||
```
|
||||
def say(message, times = 1):
|
||||
print message * times
|
||||
|
||||
say('Hello')
|
||||
say('World', 5)
|
||||
```
|
||||
|
||||
## 5.2 关键参数
|
||||
|
||||
使用名字而不是位置来指定参数
|
||||
|
||||
```
|
||||
def func(a, b=5, c=10):
|
||||
print 'a is', a, 'and b is', b, 'and c is', c
|
||||
|
||||
func(3, 7)
|
||||
func(25, c=24)
|
||||
func(c=50, a=100)
|
||||
```
|
||||
|
||||
## 5.3 return
|
||||
|
||||
没有 return 语句的函数默认 return None ,None 在 Python 中是指一个没有任何值的特殊类型。
|
||||
|
||||
## 5.4 DocStrings
|
||||
|
||||
使用函数的 \_\_doc\_\_ 属性可以返回函数中的文档字符串。
|
||||
|
||||
```
|
||||
def printMax(x, y):
|
||||
'''Prints the maximum of two numbers.
|
||||
The two values must be integers.'''
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
if x > y:
|
||||
print x, 'is maximum'
|
||||
else:
|
||||
print y, 'is maximum'
|
||||
|
||||
printMax(3, 5)
|
||||
print printMax.__doc__
|
||||
```
|
||||
|
||||
# 第 6 章 模块
|
||||
|
||||
模块基本上就是一个包含定义函数和变量的文件,使用 import 语句导入一个模块;
|
||||
|
||||
```
|
||||
import sys
|
||||
```
|
||||
|
||||
.pyc 是字节文件,会比源文件快很多,而且与平台无关。
|
||||
|
||||
使用 from sys import argv 之后可以直接在代码中使用 argv ,而不用 sys.argv。但是为了易读性,最好不要用这种方式。
|
||||
|
||||
\_\_name\_\_ 属性为一个模块的名字。
|
||||
|
||||
```
|
||||
if __name__ == '__main__':
|
||||
print 'This program is being run by itself'
|
||||
else:
|
||||
print 'I am being imported from another module
|
||||
```
|
||||
|
||||
创建一个 python 文件之后,要使得另一个 python 文件可以使用它,需要把它放在这个文件的同目录下,或者把它放到 sys.path 目录下。
|
||||
|
||||
使用 . 运算符来引用模块中的成员。
|
||||
|
||||
dir() 函数可以列出一个模块的函数、变量和类。
|
||||
|
||||
# 第 7 章 数据结构
|
||||
|
||||
## 7.1 列表
|
||||
|
||||
列表是处理一组有序的集合,并不说排好序了,而是有顺序。使用 [ ] 来定义一个列表。
|
||||
|
||||
```python
|
||||
# This is my shopping list
|
||||
shoplist = ['apple', 'mango', 'carrot', 'banana']
|
||||
print 'I have', len(shoplist),'items to purchase.'
|
||||
print 'These items are:', # Notice the comma at end of the line
|
||||
for item in shoplist:
|
||||
print item,
|
||||
shoplist.append('rice')
|
||||
shoplist.sort()
|
||||
del shoplist[0]
|
||||
```
|
||||
|
||||
## 7.2 元组
|
||||
|
||||
元组不可变。
|
||||
|
||||
一个元组可以包含另一个元组。
|
||||
|
||||
使用 ( ) 定义一个元组。
|
||||
|
||||
```
|
||||
zoo = ('wolf', 'elephant', 'penguin')
|
||||
new_zoo = ('monkey', 'dolphin', zoo)
|
||||
print 'Last animal brought from old zoo is', new_zoo[2][2]
|
||||
```
|
||||
|
||||
元组可以用在打印语句中。
|
||||
|
||||
```
|
||||
age = 22
|
||||
name = 'Swaroop'
|
||||
print '%s is %d years old' % (name, age)
|
||||
print 'Why is %s playing with that python?' % name
|
||||
```
|
||||
|
||||
## 7.3 字典
|
||||
|
||||
必须使用不可变的对象作为键。
|
||||
|
||||
d = {key1 : value1, key2 : value2 } 定义一个字典。
|
||||
|
||||
items() 方法返回所有 key。
|
||||
|
||||
has_key() 方法判断是否含有一个 key。
|
||||
|
||||
## 7.4 序列
|
||||
|
||||
字符串,元组和列表都是序列,序列主要具有索引和切片操作;
|
||||
|
||||
简单复制语句不会创建拷贝,要拷贝一个序列时用切片操作
|
||||
|
||||
```
|
||||
mylist = shoplist[:]
|
||||
```
|
||||
|
||||
## 7.5 字符串
|
||||
|
||||
startwith()
|
||||
|
||||
in 操作符可以判断一个给定字符串是否在另一个字符串中。
|
||||
|
||||
find() 查找一个字符串在另一个字符串中的位置,如果查找失败返回 -1。
|
||||
|
||||
# 第 8 章 面向对象
|
||||
|
||||
## 8.1 self
|
||||
|
||||
self 类似于 java 中的 this 指针。
|
||||
|
||||
## 8.2 类
|
||||
|
||||
```
|
||||
class Person:
|
||||
pass # An empty block
|
||||
|
||||
p = Person()
|
||||
print p
|
||||
```
|
||||
|
||||
## 8.3 对象的方法
|
||||
|
||||
方法和函数最主要的区别是方法需要传入一个 self 变量。
|
||||
|
||||
```
|
||||
class Person:
|
||||
def sayHi(self):
|
||||
print 'Are you OK?'
|
||||
|
||||
leijun = Person()
|
||||
p.sayHi()
|
||||
```
|
||||
|
||||
## 8.4 \_\_init\_\_()
|
||||
|
||||
构造函数
|
||||
|
||||
```
|
||||
class Person:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
def sayHi(self):
|
||||
print 'Hello, my name is', self.name
|
||||
|
||||
p = Person('Swaroop')
|
||||
p.sayHi()
|
||||
```
|
||||
|
||||
## 8.5 类与对象的方法
|
||||
|
||||
成员名有双下划线前缀 \_\_ ,比如 \_\_init\_\_(),则为私有成员。如果自己定义的类中某个成员想定义为私有成员,则在该成员名前加单下划线 \_,虽然 Python 没有规定这样做可以把成员变成私有成员,但是可以作为一种编程约定来使用。
|
||||
|
||||
## 8.6 \_\_del\_\_()
|
||||
|
||||
析构函数
|
||||
|
||||
## 8.7 继承
|
||||
|
||||
```
|
||||
class Person:
|
||||
def __init__(self, name, age):
|
||||
self.__name = name
|
||||
self.__age = age
|
||||
|
||||
class Worker(Person):
|
||||
def __init__(self, name, age, salary):
|
||||
Person.__init__(self, name, age)
|
||||
self.salary = salary
|
||||
```
|
||||
|
||||
# 第 9 章 输入 / 输出
|
||||
|
||||
## 9.1 文件
|
||||
|
||||
```
|
||||
poem = '''\
|
||||
Programming is fun
|
||||
When the work is done
|
||||
if you wanna make your work also fun:
|
||||
use Python!
|
||||
'''
|
||||
f = file('poem.txt', 'w')
|
||||
f.write(poem)
|
||||
f.close()
|
||||
|
||||
f = file('poem.txt')
|
||||
while True:
|
||||
line = f.readline()
|
||||
if len(line) == 0:
|
||||
break
|
||||
print line
|
||||
f.close()
|
||||
|
||||
```
|
||||
|
||||
## 9.2 序列化
|
||||
|
||||
pickle 模块可以用于存取一个对象。
|
||||
|
||||
cPickle 功能完全相同,但是用 c 语言编写的,速度快 1000 倍;
|
||||
|
||||
```
|
||||
import cPickle as p
|
||||
|
||||
shoplistfile = 'shoplist.data'
|
||||
shoplist = ['apple', 'mango', 'carrot']
|
||||
|
||||
f = file(shoplistfile, 'w')
|
||||
p.dump(shoplist, f)
|
||||
f.close()
|
||||
|
||||
del shoplist
|
||||
|
||||
f = file(shoplistfile)
|
||||
storedlist = p.load(f)
|
||||
print storedlist
|
||||
|
||||
```
|
||||
|
||||
# 第 10 章 异常
|
||||
|
||||
## 10.1 try-except
|
||||
|
||||
可以跟上一个 else 从句,当没有异常发生时会执行;
|
||||
|
||||
```
|
||||
import sys
|
||||
try:
|
||||
s = raw_input('Enter something --> ')
|
||||
except EOFError:
|
||||
print '\nWhy did you do an EOF on me?'
|
||||
sys.exit() # exit the program
|
||||
except:
|
||||
print '\nSome error/exception occurred.'
|
||||
# here, we are not exiting the program
|
||||
print 'Done'
|
||||
|
||||
```
|
||||
|
||||
## 10.2 引发异常
|
||||
|
||||
raise 语句可以引发一个异常,引发的异常应该是 Error 或者 Exception 的类或者子类;
|
||||
|
||||
```
|
||||
class MyException(Exception):
|
||||
def __init__(self):
|
||||
self.name = 'MyException'
|
||||
|
||||
try:
|
||||
raise MyException
|
||||
except MyException, e:
|
||||
print e.name
|
||||
else:
|
||||
print 'nothing'
|
||||
```
|
||||
|
||||
## 10.3 finally
|
||||
|
||||
finally 可以保证一个语句块无论如何都会执行。
|
||||
|
||||
# 第 11 章 Python 标准库
|
||||
|
||||
## 11.1 sys 模块
|
||||
|
||||
sys.argv 参数列表。
|
||||
|
||||
sys.stdin sys.stdout sys.stderr。
|
||||
|
||||
## 11.2 os 模块
|
||||
|
||||
该模块包含了普遍的操作系统功能,使程序能够与平台无关;
|
||||
|
||||
os.name 字符串指示你正在使用的平台。比如对于 Windows,它是 'nt' ,而对于 Linux/Unix 用户,它是 'posix'。
|
||||
|
||||
os.getcwd() 函数得到当前工作目录,即当前 Python 脚本工作的目录路径。
|
||||
|
||||
os.getenv() 和 os.putenv() 函数分别用来读取和设置环境变量。
|
||||
|
||||
os.listdir() 返回指定目录下的所有文件和目录名。
|
||||
|
||||
os.remove() 函数用来删除一个文件。
|
||||
|
||||
os.system() 函数用来运行 shell 命令。
|
||||
|
||||
os.linesep 字符串给出当前平台使用的行终止符。例如,Windows 使用 '\r\n',Linux 用 '\n' 而 Mac 使用 '\r'。
|
||||
|
||||
os.path.split() 函数返回一个路径的目录名和文件名。
|
||||
|
||||
```
|
||||
os.path.split('/home/swaroop/byte/code/poem.txt')
|
||||
('/home/swaroop/byte/code', 'poem.txt')
|
||||
```
|
||||
|
||||
os.path.isfile() 和 os.path.isdir() 函数分别检验给出的路径是一个文件还是目录。类似地,os.path.exists() 函数用来检验给出的路径是否真地存在。
|
||||
|
||||
# 第 12 章 更多 Python 的内容
|
||||
|
||||
## 12.1 特殊的方法
|
||||
|
||||
特殊的方法主要用于模仿某个行为。
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| __init__(self,...) | 这个方法在新建对象恰好要被返回使用之前被调用。 |
|
||||
| __del__(self) | 恰好在对象要被删除之前调用。 |
|
||||
| __str__(self) | 在我们对对象使用 print 语句或是使用 str() 的时候调用。 |
|
||||
| __lt__(self, other) | 当使用 小于 运算符(<)的时候调用。类似地,对于所有的运算符(+,> 等等)都有特殊的方法。 |
|
||||
| __getitem__(self, key) | 使用 x[key] 索引操作符的时候调用。 |
|
||||
| __len__(self) | 对序列对象使用内建的 len() 函数的时候调用。 |
|
||||
|
||||
## 12.2 列表综合
|
||||
|
||||
```
|
||||
listone = [2, 3, 4]
|
||||
listtwo = [2*i for i in listone if i > 2]
|
||||
print listtwo
|
||||
```
|
||||
|
||||
## 12.3 在函数中接受元组和列表
|
||||
|
||||
```
|
||||
def powersum(power, *args):
|
||||
total = 0
|
||||
for i in args:
|
||||
total += pow(i, power)
|
||||
return tota
|
||||
```
|
||||
|
||||
由于在 args 变量前有 \* 前缀,所有多余的函数参数都会作为一个元组存储在 args 中。如果使用的是 \*\* 前缀,多余的参数则会被认为是一个字典的 键 / 值 对。
|
||||
|
||||
## 12.4 lambada 形式
|
||||
|
||||
lambda 语句被用来创建新的函数对象,并且在运行时返回它们。
|
||||
|
||||
```
|
||||
def make_repeater(n):
|
||||
return lambda s: s*n
|
||||
twice = make_repeater(2)
|
||||
print twice('word')
|
||||
print twice(5)
|
||||
|
||||
wordword
|
||||
10
|
||||
```
|
||||
|
||||
## 12.5 exec 和 eval()
|
||||
|
||||
```
|
||||
exec 'print "Hello World"'
|
||||
eval('2*3')
|
||||
```
|
||||
|
||||
## 12.6 assert 语句
|
||||
|
||||
asser t 断言在非真时会引发一个 AssertionErro。
|
||||
|
||||
```
|
||||
mylist = ['item']
|
||||
assert len(mylist) >= 1
|
||||
```
|
||||
|
||||
## 12.7 repr() 函数
|
||||
|
||||
repr() 函数和反引号用来获取对象的可打印的表示形式。
|
||||
|
||||
可以通过定义类的\_\_repr\_\_() 方法来控制你的对象在被 repr 函数调用的时候返回的内容。
|
||||
|
||||
```
|
||||
>>> i = []
|
||||
>>> i.append('item')
|
||||
>>> `i`
|
||||
"['item']"
|
||||
>>> repr(i)
|
||||
"['item']
|
||||
```
|
||||
|
||||
# 参考资料
|
||||
|
||||
1. 《简明 Python 教程》
|
@ -1,471 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
|
||||
# 实现单例模式
|
||||
|
||||
## 1 使用\_\_new\_\_方法
|
||||
|
||||
```python
|
||||
class Singleton(object):
|
||||
def __new__(cls, *args, **kw):
|
||||
if not hasattr(cls, '_instance'):
|
||||
orig = super(Singleton, cls)
|
||||
cls._instance = orig.__new__(cls, *args, **kw)
|
||||
return cls._instance
|
||||
|
||||
class MyClass(Singleton):
|
||||
a = 1
|
||||
```
|
||||
|
||||
## 2 共享属性
|
||||
|
||||
创建实例时把所有实例的`__dict__`指向同一个字典 , 这样它们具有相同的属性和方法 .
|
||||
|
||||
```python
|
||||
|
||||
class Borg(object):
|
||||
_state = {}
|
||||
def __new__(cls, *args, **kw):
|
||||
ob = super(Borg, cls).__new__(cls, *args, **kw)
|
||||
ob.__dict__ = cls._state
|
||||
return ob
|
||||
|
||||
class MyClass2(Borg):
|
||||
a = 1
|
||||
```
|
||||
|
||||
## 3 装饰器版本
|
||||
|
||||
```python
|
||||
def singleton(cls, *args, **kw):
|
||||
instances = {}
|
||||
def getinstance():
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kw)
|
||||
return instances[cls]
|
||||
return getinstance
|
||||
|
||||
@singleton
|
||||
class MyClass:
|
||||
...
|
||||
```
|
||||
|
||||
## 4 import 方法
|
||||
|
||||
作为 python 的模块是天然的单例模式
|
||||
|
||||
```python
|
||||
# mysingleton.py
|
||||
class My_Singleton(object):
|
||||
def foo(self):
|
||||
pass
|
||||
|
||||
my_singleton = My_Singleton()
|
||||
|
||||
# to use
|
||||
from mysingleton import my_singleton
|
||||
|
||||
my_singleton.foo()
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# 垃圾回收机制
|
||||
|
||||
Python GC 主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用计数的基础上,通过“标记 - 清除”(mark and sweep)解决容器对象可能产生的循环引用问题,通过“分代回收”(generation collection)以空间换时间的方法提高垃圾回收效率。
|
||||
|
||||
## 1 引用计数
|
||||
|
||||
PyObject 是每个对象必有的内容,其中`ob_refcnt`就是做为引用计数。当一个对象有新的引用时,它的`ob_refcnt`就会增加,当引用它的对象被删除,它的`ob_refcnt`就会减少 . 引用计数为 0 时,该对象生命就结束了。
|
||||
|
||||
优点 :
|
||||
|
||||
1. 简单
|
||||
2. 实时性
|
||||
|
||||
缺点 :
|
||||
|
||||
1. 维护引用计数消耗资源
|
||||
2. 循环引用
|
||||
|
||||
## 2 标记 - 清除机制
|
||||
|
||||
基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。
|
||||
|
||||
## 3 分代技术
|
||||
|
||||
分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。
|
||||
|
||||
Python 默认定义了三代对象集合,索引数越大,对象存活时间越长。
|
||||
|
||||
举例:
|
||||
当某些内存块 M 经过了 3 次垃圾收集的清洗之后还存活时,我们就将内存块 M 划到一个集合 A 中去,而新分配的内存都划分到集合 B 中去。当垃圾收集开始工作时,大多数情况都只对集合 B 进行垃圾回收,而对集合 A 进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合 B 中的某些内存块由于存活时间长而会被转移到集合 A 中,当然,集合 A 中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
|
||||
|
||||
# list 实现
|
||||
|
||||
https://www.jianshu.com/p/J4U6rR
|
||||
|
||||
# Python 2 和 3 的区别
|
||||
|
||||
http://chenqx.github.io/2014/11/10/Key-differences-between-Python-2-7-x-and-Python-3-x/
|
||||
|
||||
# 去除列表中重复的数
|
||||
|
||||
```python
|
||||
a = [1, 2, 4, 2, 4, 5, 6, 5, 7, 8, 9, 0]
|
||||
a = list(set(a))
|
||||
```
|
||||
|
||||
# 替换字符串
|
||||
|
||||
使用 replace() 函数
|
||||
|
||||
```python
|
||||
str = "aaa bbb ccc"
|
||||
str = str.replace("aaa", "111")
|
||||
```
|
||||
|
||||
使用 re 模块的 sub() 函数进行查询和替换。
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
str = "aaa bbb ccc"
|
||||
rex = r'(aaa|bbb)'
|
||||
str = re.sub(rex, "111", str)
|
||||
```
|
||||
|
||||
# read, readline 和 readlines
|
||||
|
||||
- read 读取整个文件
|
||||
- readline 读取下一行 , 使用生成器方法
|
||||
- readlines 读取整个文件到一个迭代器以供我们遍历
|
||||
|
||||
# is 与 ==
|
||||
|
||||
is 比较地址,== 比较值
|
||||
|
||||
|
||||
# 可变对象与不可变对象
|
||||
|
||||
在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list, dict, set 等则是可以修改的对象。
|
||||
|
||||
当一个变量引用了一个不可变对象时,变量值发生改变,变量所引用的对象也发生改变,也就是对象引用的地址也发生了改变。
|
||||
|
||||
# 静态方法和类方法
|
||||
|
||||
Python 其实有 3 个方法 , 即静态方法 (staticmethod), 类方法 (classmethod) 和对象方法。
|
||||
|
||||
其中对象方法的第一个参数 selft,在使用类对象调用时会将对象隐式传入;类方法的第一个参数 cls,在使用类调用时会将类隐式传入;而静态方法没有隐式参数。
|
||||
|
||||
```python
|
||||
def foo(x):
|
||||
print "executing foo(%s)"%(x)
|
||||
|
||||
class A(object):
|
||||
def foo(self, x):
|
||||
print "executing foo(%s,%s)"%(self, x)
|
||||
|
||||
@classmethod
|
||||
def class_foo(cls, x):
|
||||
print "executing class_foo(%s,%s)"%(cls, x)
|
||||
|
||||
@staticmethod
|
||||
def static_foo(x):
|
||||
print "executing static_foo(%s)"%x
|
||||
|
||||
```
|
||||
|
||||
详情:[What is the difference between @staticmethod and @classmethod in Python?
|
||||
](https://stackoverflow.com/questions/136097/what-is-the-difference-between-staticmethod-and-classmethod-in-python)
|
||||
|
||||
# 拷贝
|
||||
|
||||
赋值创建了对象的一个新的引用;浅拷贝创建一个新的对象,但它包含的是对原始对象中包含项的引用,而深拷贝会递归的复制它所包含的对象。
|
||||
|
||||
创建浅拷贝的方式:
|
||||
|
||||
- 完全切片方法;
|
||||
- 工厂函数,如 list();
|
||||
- copy 模块的 copy() 函数
|
||||
|
||||
创建深拷贝的方式:
|
||||
|
||||
- copy 模块的 deepcopy() 函数
|
||||
|
||||
|
||||
```python
|
||||
import copy
|
||||
a = [1, 2, 3, 4, ['a', 'b']] # 原始对象
|
||||
|
||||
b = a # 赋值,传对象的引用
|
||||
c = copy.copy(a) # 对象拷贝,浅拷贝
|
||||
d = copy.deepcopy(a) # 对象拷贝,深拷贝
|
||||
|
||||
a.append(5) # 修改对象 a
|
||||
a[4].append('c') # 修改对象 a 中的 ['a', 'b'] 数组对象
|
||||
|
||||
print 'a = ', a
|
||||
print 'b = ', b
|
||||
print 'c = ', c
|
||||
print 'd = ', d
|
||||
|
||||
输出结果:
|
||||
a = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
|
||||
b = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
|
||||
c = [1, 2, 3, 4, ['a', 'b', 'c']]
|
||||
d = [1, 2, 3, 4, ['a', 'b']]
|
||||
```
|
||||
|
||||
# 作用域
|
||||
|
||||
当 Python 遇到一个变量的话他会按照这样的顺序进行搜索:
|
||||
|
||||
本地作用域(Local)→当前作用域被嵌入的本地作用域(Enclosing locals)→全局 / 模块作用域(Global)→内置作用域(Built-in)
|
||||
|
||||
|
||||
|
||||
# 类变量与实例变量
|
||||
|
||||
```python
|
||||
class Test(object):
|
||||
num_of_instance = 0
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
Test.num_of_instance += 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
print Test.num_of_instance # 0
|
||||
t1 = Test('jack')
|
||||
print Test.num_of_instance # 1
|
||||
t2 = Test('lucy')
|
||||
print t1.name , t1.num_of_instance # jack 2
|
||||
print t2.name , t2.num_of_instance # lucy 2
|
||||
```
|
||||
|
||||
# 自省
|
||||
|
||||
类似于 Java 的反射
|
||||
|
||||
```python
|
||||
a = [1, 2, 3]
|
||||
b = {'a':1,'b':2,'c':3}
|
||||
c = True
|
||||
print type(a), type(b), type(c) # <type 'list'> <type 'dict'> <type 'bool'>
|
||||
print isinstance(a, list) # True
|
||||
```
|
||||
|
||||
# 字典推导式
|
||||
|
||||
```python
|
||||
d = {key: value for (key, value) in iterable}
|
||||
```
|
||||
|
||||
# 单下划线和双下划线
|
||||
|
||||
```python
|
||||
>>> class MyClass():
|
||||
... def __init__(self):
|
||||
... self.__superprivate = "Hello"
|
||||
... self._semiprivate = ", world!"
|
||||
...
|
||||
>>> mc = MyClass()
|
||||
>>> print mc.__superprivate
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
AttributeError: myClass instance has no attribute '__superprivate'
|
||||
>>> print mc._semiprivate
|
||||
, world!
|
||||
>>> print mc.__dict__
|
||||
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}
|
||||
```
|
||||
|
||||
\_\_foo\_\_: 一种约定 , Python 内部的名字 , 用来区别其他用户自定义的命名 , 以防冲突,就是例如\_\_init\_\_(),\_\_del\__(),\__call\__() 这些特殊方法
|
||||
|
||||
\_foo: 一种约定 , 用来指定变量私有 . 程序员用来指定私有变量的一种方式 . 不能用 from module import * 导入,其他方面和公有一样访问;
|
||||
|
||||
\_\_foo: 这个有真正的意义 : 解析器用\_classname\_\_foo 来代替这个名字 , 以区别和其他类相同的命名 , 它无法直接像公有成员一样随便访问 , 通过对象名 .\_类名\_\_xxx 这样的方式可以访问 .
|
||||
|
||||
# 迭代器和生成器
|
||||
|
||||
这里有个关于生成器的创建问题面试官有考: 问: 将列表生成式中 [] 改成 () 之后数据结构是否改变? 答案:是,从列表变为生成器
|
||||
|
||||
```python
|
||||
>>> L = [x*x for x in range(10)]
|
||||
>>> L
|
||||
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
|
||||
>>> g = (x*x for x in range(10))
|
||||
>>> g
|
||||
<generator object <genexpr> at 0x0000028F8B774200>
|
||||
```
|
||||
|
||||
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。因此,没有必要创建完整的列表(节省大量内存空间)。在 Python 中,我们可以采用生成器:边循环,边计算的机制—>generator
|
||||
|
||||
http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do-in-python
|
||||
|
||||
10 \*args and \*\*kwargs
|
||||
用\*args 和\*\*kwargs 只是为了方便并没有强制使用它们 .
|
||||
|
||||
当你不确定你的函数里将要传递多少参数时你可以用\*args. 例如 , 它可以传递任意数量的参数 :
|
||||
```python
|
||||
>>> def print_everything(*args):
|
||||
for count, thing in enumerate(args):
|
||||
... print '{0}. {1}'.format(count, thing)
|
||||
...
|
||||
>>> print_everything('apple', 'banana', 'cabbage')
|
||||
0. apple
|
||||
1. banana
|
||||
2. cabbage
|
||||
```
|
||||
相似的 ,\*\*kwargs 允许你使用没有事先定义的参数名 :
|
||||
```python
|
||||
>>> def table_things(**kwargs):
|
||||
... for name, value in kwargs.items():
|
||||
... print '{0} = {1}'.format(name, value)
|
||||
...
|
||||
>>> table_things(apple = 'fruit', cabbage = 'vegetable')
|
||||
cabbage = vegetable
|
||||
apple = fruit
|
||||
```
|
||||
你也可以混着用 . 命名参数首先获得参数值然后所有的其他参数都传递给\*args 和\*\*kwargs. 命名参数在列表的最前端 . 例如 :
|
||||
|
||||
```python
|
||||
def table_things(titlestring, **kwargs)
|
||||
```
|
||||
\*args 和*\\*kwargs 可以同时在函数的定义中 , 但是\*args 必须在\**\kwargs 前面 .
|
||||
|
||||
当调用函数时你也可以用\*和\*\*语法 . 例如 :
|
||||
```python
|
||||
>>> def print_three_things(a, b, c):
|
||||
... print 'a = {0}, b = {1}, c = {2}'.format(a, b, c)
|
||||
...
|
||||
>>> mylist = ['aardvark', 'baboon', 'cat']
|
||||
>>> print_three_things(*mylist)
|
||||
|
||||
a = aardvark, b = baboon, c = cat
|
||||
```
|
||||
|
||||
就像你看到的一样 , 它可以传递列表 ( 或者元组 ) 的每一项并把它们解包 . 注意必须与它们在函数里的参数相吻合 . 当然 , 你也可以在函数定义或者函数调用时用*.
|
||||
|
||||
http://stackoverflow.com/questions/3394835/args-and-kwargs
|
||||
|
||||
# 面向切面编程 AOP 和装饰器
|
||||
|
||||
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
|
||||
|
||||
这个问题比较大 , 推荐 : http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python
|
||||
|
||||
# 鸭子类型
|
||||
|
||||
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
|
||||
|
||||
我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。
|
||||
|
||||
比如在 python 中,有很多 file-like 的东西,比如 StringIO, GzipFile, socket。它们有很多相同的方法,我们把它们当作文件使用。
|
||||
|
||||
又比如 list.extend() 方法中 , 我们并不关心它的参数是不是 list, 只要它是可迭代的 , 所以它的参数可以是 list/tuple/dict/ 字符串 / 生成器等 .
|
||||
|
||||
鸭子类型在动态语言中经常使用,非常灵活,使得 python 不想 java 那样专门去弄一大堆的设计模式。
|
||||
|
||||
# 重载
|
||||
|
||||
缺省参数实现重载。
|
||||
|
||||
http://www.zhihu.com/question/20053359
|
||||
|
||||
# 新式类和旧式类
|
||||
|
||||
这篇文章很好的介绍了新式类的特性 : http://www.cnblogs.com/btchenguang/archive/2012/09/17/2689146.html
|
||||
|
||||
新式类很早在 2.2 就出现了 , 所以旧式类完全是兼容的问题 , Python3 里的类全部都是新式类 . 这里有一个 MRO 问题可以了解下 ( 新式类是广度优先 , 旧式类是深度优先 ),<Python 核心编程 > 里讲的也很多 .
|
||||
|
||||
一个旧式类的深度优先的例子
|
||||
|
||||
```python
|
||||
class A():
|
||||
def foo1(self):
|
||||
print "A"
|
||||
class B(A):
|
||||
def foo2(self):
|
||||
pass
|
||||
class C(A):
|
||||
def foo1(self):
|
||||
print "C"
|
||||
class D(B, C):
|
||||
pass
|
||||
|
||||
d = D()
|
||||
d.foo1()
|
||||
|
||||
# A
|
||||
```
|
||||
|
||||
**按照经典类的查找顺序`从左到右深度优先`的规则,在访问`d.foo1()`的时候 , D 这个类是没有的 .. 那么往上查找 , 先找到 B, 里面没有 , 深度优先 , 访问 A, 找到了 foo1(), 所以这时候调用的是 A 的 foo1(),从而导致 C 重写的 foo1() 被绕过**
|
||||
|
||||
__new__是一个静态方法 , 而__init__是一个实例方法 .
|
||||
__new__方法会返回一个创建的实例 , 而__init__什么都不返回 .
|
||||
只有在__new__返回一个 cls 的实例时后面的__init__才能被调用 .
|
||||
当创建一个新实例时调用__new__, 初始化一个实例时用__init__.
|
||||
|
||||
# `__new__`和`__init__`的区别
|
||||
|
||||
这个`__new__`确实很少见到 , 先做了解吧 .
|
||||
|
||||
1. `__new__`是一个静态方法 , 而`__init__`是一个实例方法 .
|
||||
2. `__new__`方法会返回一个创建的实例 , 而`__init__`什么都不返回 .
|
||||
3. 只有在`__new__`返回一个 cls 的实例时后面的`__init__`才能被调用 .
|
||||
4. 当创建一个新实例时调用`__new__`, 初始化一个实例时用`__init__`.
|
||||
|
||||
[stackoverflow](http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init)
|
||||
|
||||
# lambda 函数
|
||||
|
||||
其实就是一个匿名函数 , 为什么叫 lambda? 因为和后面的函数式编程有关 .
|
||||
|
||||
推荐 : [ 知乎 ](http://www.zhihu.com/question/20125256)
|
||||
|
||||
# Python 函数式编程
|
||||
|
||||
推荐 : [ 酷壳 ](http://coolshell.cn/articles/10822.html)
|
||||
|
||||
python 中函数式编程支持 :
|
||||
|
||||
filter 函数的功能相当于过滤器。调用一个布尔函数 bool_func 来迭代遍历每个 seq 中的元素;返回一个使 bool_seq 返回值为 true 的元素的序列。
|
||||
|
||||
```python
|
||||
>>>a = [1, 2, 3, 4, 5, 6, 7]
|
||||
>>>b = filter(lambda x: x > 5, a)
|
||||
>>>print b
|
||||
>>>[6, 7]
|
||||
```
|
||||
|
||||
map 函数是对一个序列的每个项依次执行函数,下面是对一个序列每个项都乘以 2:
|
||||
|
||||
```python
|
||||
>>> a = map(lambda x:x*2,[1, 2, 3])
|
||||
>>> list(a)
|
||||
[2, 4, 6]
|
||||
```
|
||||
|
||||
reduce 函数是对一个序列的每个项迭代调用函数,下面是求 3 的阶乘:
|
||||
|
||||
```python
|
||||
>>> reduce(lambda x, y:x*y, range(1, 4))
|
||||
6
|
||||
```
|
||||
|
||||
# 闭包
|
||||
|
||||
闭包 (closure) 是函数式编程的重要的语法结构。
|
||||
|
||||
创建一个闭包必须满足以下几点 :
|
||||
|
||||
1. 必须有一个内嵌函数
|
||||
2. 内嵌函数必须引用外部函数中的变量
|
||||
3. 外部函数的返回值必须是内嵌函数
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [Github:interview_python](https://github.com/taizilongxu/interview_python)
|
@ -1,284 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# Redis是什么
|
||||
|
||||
Redis 是速度非常快的非关系型(nosql)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
|
||||
|
||||
五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
|
||||
|
||||
Redis 支持很多特性,例如可以将内存中的数据持久化到硬盘中,可以使用复制来扩展读性能,可以使用分片来扩展写性能。
|
||||
|
||||
# Redis的五种基本类型
|
||||
|
||||
| 数据类型 | 可以存储的值 | 操作 |
|
||||
| -- | -- | -- |
|
||||
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作;</br> 对整数和浮点数执行自增或者自减操作 |
|
||||
| LIST | 链表 | 从两端压入或者弹出元素;</br> 读取单个或者多个元素;</br> 进行修剪,只保留一个范围内的元素。 |
|
||||
| SET | 无序集合 | 添加、获取、移除单个元素;</br> 检查一个元素是否存在于集合中;</br> 计算交集、并集、差集;</br> 从集合里面随机获取元素。 |
|
||||
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对;</br> 获取所有键值对;</br> 检查某个键是否存在。|
|
||||
| ZSET | 有序集合<sup>1</sup> | 添加、获取、删除元素个元素;</br> 根据分值范围或者成员来获取元素;</br> 计算一个键的排名。 |
|
||||
|
||||
注 1:有序集合的每个集合元素都对应一个分值,根据这个分值的大小来对集合元素进行排序。有因此有序集合相当于是有序的散列表,键是集合元素,值为元素对应的分值。
|
||||
|
||||
# 键的过期时间
|
||||
|
||||
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
|
||||
|
||||
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
|
||||
|
||||
过期时间对于清理缓存数据非常有用。
|
||||
|
||||
# 发布与订阅
|
||||
|
||||
发布与订阅实际上是观察者模式,订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
|
||||
|
||||
发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下:
|
||||
|
||||
1. 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多;
|
||||
2. 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。
|
||||
|
||||
# 事务
|
||||
|
||||
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
|
||||
|
||||
MULTI 和 EXEC 中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
|
||||
|
||||
# 持久化
|
||||
|
||||
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
||||
|
||||
## 1. 快照持久化
|
||||
|
||||
将某个时间点的所有数据都存放到硬盘上。
|
||||
|
||||
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
|
||||
|
||||
如果系统发生故障,将会丢失最后一次创建快照之后的数据。并且如果数据量很大,保存快照的时间也会很长。
|
||||
|
||||
## 2. AOF 持久化
|
||||
|
||||
AOF 持久化将写命令添加到 AOF 文件(Append Only File)的末尾。
|
||||
|
||||
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。因此将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项:
|
||||
|
||||
| 选项 | 同步频率 |
|
||||
| -- | -- |
|
||||
| always | 每个写命令都同步 |
|
||||
| everysec | 每秒同步一次 |
|
||||
| no | 让操作系统来决定何时同步 |
|
||||
|
||||
always 选项会严重减低服务器的性能;everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
|
||||
|
||||
随着服务器写请求的增多,AOF 文件会越来越大;Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
|
||||
|
||||
# 复制
|
||||
|
||||
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
|
||||
|
||||
一个从服务器只能有一个主服务器,并且不支持主主复制。
|
||||
|
||||
**1. 从服务器连接主服务器的过程**
|
||||
|
||||
(1) 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
|
||||
(2) 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
|
||||
(3) 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
|
||||
|
||||
**2. 主从链**
|
||||
|
||||
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器而导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
|
||||
|
||||

|
||||
|
||||
# 处理故障
|
||||
|
||||
要用到持久化文件来恢复服务器的数据。
|
||||
|
||||
持久化文件可能因为服务器出错也有错误,因此要先对持久化文件进行验证和修复。对 AOF 文件就行验证和修复很容易,修复操作将第一个出错命令和其后的所有命令都删除;但是只能验证快照文件,无法对快照文件进行修复,因为快照文件进行了压缩,出现在快照文件中间的错误可能会导致整个快照文件的剩余部分无法读取。
|
||||
|
||||
当主服务器出现故障时,Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器,B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C,并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。
|
||||
|
||||
# 分片
|
||||
|
||||
Redis 中的分片类似于 MySQL 的分表操作,分片是将数据划分为多个部分的方法,对数据的划分可以基于键包含的 ID、基于键的哈希值,或者基于以上两者的某种组合。通过对数据进行分片,用户可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
|
||||
|
||||
假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
|
||||
|
||||
**1. 客户端分片**
|
||||
|
||||
客户端使用一致性哈希等算法决定键应当分布到哪个节点。
|
||||
|
||||
**2. 代理分片**
|
||||
|
||||
将客户端请求发送到代理上,由代理转发请求到正确的节点上。
|
||||
|
||||
**3. 服务器分片**
|
||||
|
||||
Redis Cluster。
|
||||
|
||||
# 事件
|
||||
|
||||
**1. 事件类型**
|
||||
|
||||
(1) 文件事件:服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件;
|
||||
|
||||
(2) 时间事件:又分为两类,定时事件是让一段程序在指定的时间之内执行一次;周期性时间是让一段程序每隔指定时间就执行一次。
|
||||
|
||||
**2. 事件的调度与执行**
|
||||
|
||||
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
|
||||
|
||||
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
|
||||
|
||||
```python
|
||||
def aeProcessEvents():
|
||||
|
||||
# 获取到达时间离当前时间最接近的时间事件
|
||||
time_event = aeSearchNearestTimer()
|
||||
|
||||
#计算最接近的时间事件距离到达还有多少毫秒
|
||||
remaind_ms = time_event.when - unix_ts_now()
|
||||
|
||||
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
|
||||
if remaind_ms < 0:
|
||||
remaind_ms = 0
|
||||
|
||||
# 根据 remaind_ms 的值,创建 timeval
|
||||
timeval = create_timeval_with_ms(remaind_ms)
|
||||
|
||||
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
|
||||
aeApiPoll(timeval)
|
||||
|
||||
# 处理所有已产生的文件事件
|
||||
procesFileEvents()
|
||||
|
||||
# 处理所有已到达的时间事件
|
||||
processTimeEvents()
|
||||
```
|
||||
|
||||
将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
|
||||
|
||||
```python
|
||||
def main():
|
||||
|
||||
# 初始化服务器
|
||||
init_server()
|
||||
|
||||
# 一直处理事件,直到服务器关闭为止
|
||||
while server_is_not_shutdown():
|
||||
aeProcessEvents()
|
||||
|
||||
# 服务器关闭,执行清理操作
|
||||
clean_server()
|
||||
```
|
||||
|
||||
事件处理的角度下服务器运行流程如下:
|
||||
|
||||

|
||||
|
||||
# Redis与Memcached的区别
|
||||
|
||||
两者都是非关系型内存键值数据库。有以下主要不同:
|
||||
|
||||
**1. 数据类型**
|
||||
|
||||
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
|
||||
|
||||
**2. 数据持久化**
|
||||
|
||||
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
|
||||
|
||||
**3. 分布式**
|
||||
|
||||
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
||||
|
||||
Redis Cluster 实现了分布式的支持。
|
||||
|
||||
**4. 内存管理机制**
|
||||
|
||||
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
|
||||
|
||||
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
||||
|
||||
# Redis适用场景
|
||||
|
||||
**1. 缓存**
|
||||
|
||||
适用 Redis 作为缓存,将热点数据放到内存中。
|
||||
|
||||
**2. 消息队列**
|
||||
|
||||
Redis 的 list 类型是双向链表,很适合用于消息队列。
|
||||
|
||||
**3. 计数器**
|
||||
|
||||
Redis 这种内存数据库才能支持计数器的频繁读写操作。
|
||||
|
||||
**4. 好友关系**
|
||||
|
||||
使用 set 类型的交集很容易就可以知道两个用户的共同好友。
|
||||
|
||||
# 数据淘汰策略
|
||||
|
||||
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
|
||||
|
||||
| 策略 | 描述 |
|
||||
| -- | -- |
|
||||
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
|
||||
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|
||||
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
|
||||
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
|
||||
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
||||
| no-envicition | 禁止驱逐数据 |
|
||||
|
||||
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
||||
|
||||
具体问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
|
||||
|
||||
# 一个简单的论坛系统分析
|
||||
|
||||
该论坛系统功能如下:
|
||||
|
||||
1. 可以发布文章;
|
||||
2. 可以对文章进行点赞;
|
||||
3. 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
|
||||
|
||||
**1. 文章信息**
|
||||
|
||||
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
|
||||
|
||||
Redis 没有表的概念将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。
|
||||
|
||||

|
||||
|
||||
**2. 点赞功能**
|
||||
|
||||
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户不断点赞。可以建立文章的已投票用户集合来进行记录。
|
||||
|
||||
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
|
||||
|
||||

|
||||
|
||||
**3. 对文章进行排序**
|
||||
|
||||
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据它们间接计算出来的)
|
||||
|
||||

|
||||
|
||||
# 案例分析
|
||||
|
||||
**1. 假设有一个博客系统,数据库存储采用 MySQL,用户数量为 1000 万,预计文章总数为 10 亿,每天有至少 10 万的更新量,每天访问量为 5000 万,对数据库的读写操作的比例超过 10:1,你如何设计该系统以确保系统高效、稳定的运行?**
|
||||
|
||||
提示:可以从数据库设计、系统框架、及网络架构方面进行描述,可以自由发挥。
|
||||
|
||||
① 考虑到读操作比写操作多的多,因此可以使用主从架构,让主服务器处理写请求,从服务器处理读请求,并且从服务器可以使用读性能比较高的 MyISAM 存储引擎;
|
||||
② 博客系统有个特点,就是热点数据特别明显,比如精华文章、推荐文章等,对这部分热点数据可以使用 Redis 进行缓存;
|
||||
③ 由于文章总数很大,可以选择根据用户散列来进行分表操作。
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
1. Redis实战
|
||||
2. Reids设计与实现
|
||||
3. [论述Redis和Memcached的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
|
||||
4. [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
|
||||
5. [Redis应用场景](http://www.scienjus.com/redis-use-case/)
|
@ -1,225 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 查找最晚入职员工的所有信息
|
||||
|
||||
```sql
|
||||
select *
|
||||
from employees
|
||||
order by hire_date desc
|
||||
limit 1;
|
||||
```
|
||||
|
||||
# 查找入职员工时间排名倒数第三的员工所有信息
|
||||
|
||||
```sql
|
||||
select *
|
||||
from employees
|
||||
order by hire_date desc
|
||||
limit 2, 1;
|
||||
```
|
||||
|
||||
# 查找各个部门当前领导当前薪水详情以及其对应部门编号dept_no
|
||||
|
||||
```sql
|
||||
select s.emp_no, s.salary, s.from_date, s.to_date, d.dept_no
|
||||
from salaries as s inner join dept_manager as d
|
||||
on d.emp_no = s.emp_no
|
||||
and d.to_date = '9999-01-01'
|
||||
and s.to_date = '9999-01-01';
|
||||
```
|
||||
|
||||
# 查找所有已经分配部门的员工的last_name和first_name
|
||||
|
||||
```sql
|
||||
select e.last_name, e.first_name, d.dept_no
|
||||
from employees as e inner join dept_emp as d
|
||||
on e.emp_no = d.emp_no;
|
||||
```
|
||||
|
||||
# 查找所有员工的last_name和first_name以及对应部门编号dept_no
|
||||
|
||||
也包括展示没有分配具体部门的员工
|
||||
|
||||
```sql
|
||||
select e.last_name, e.first_name, d.dept_no
|
||||
from employees as e left outer join dept_emp as d
|
||||
on e.emp_no = d.emp_no;
|
||||
```
|
||||
|
||||
# 查找所有员工入职时候的薪水情况
|
||||
|
||||
```sql
|
||||
select e.emp_no, s.salary
|
||||
from employees as e inner join salaries as s
|
||||
on e.emp_no = s.emp_no and e.hire_date = s.from_date
|
||||
order by e.emp_no desc;
|
||||
```
|
||||
|
||||
# 查找薪水涨幅超过15次的员工号emp_no以及其对应的涨幅次数t
|
||||
|
||||
```sql
|
||||
select emp_no, count(*) as t
|
||||
from salaries
|
||||
group by emp_no
|
||||
having t > 15;
|
||||
```
|
||||
|
||||
# 找出所有员工当前具体的薪水salary情况
|
||||
|
||||
```sql
|
||||
select distinct salary
|
||||
from salaries
|
||||
where to_date = '9999-01-01'
|
||||
order by salary desc;
|
||||
```
|
||||
|
||||
# 获取所有部门当前manager的当前薪水情况
|
||||
|
||||
```sql
|
||||
select d.dept_no, d.emp_no, s.salary
|
||||
from salaries as s inner join dept_manager as d
|
||||
on d.emp_no = s.emp_no
|
||||
and d.to_date = '9999-01-01'
|
||||
and s.to_date = '9999-01-01';
|
||||
```
|
||||
|
||||
# 获取所有非manager的员工emp_no
|
||||
|
||||
```sql
|
||||
select emp_no
|
||||
from employees
|
||||
where emp_no not in (
|
||||
select emp_no
|
||||
from dept_manager
|
||||
)
|
||||
```
|
||||
|
||||
# 获取所有员工当前的manager
|
||||
|
||||
```sql
|
||||
select d1.emp_no, d2.emp_no as manager_no
|
||||
from dept_emp as d1 inner join dept_manager as d2
|
||||
on d1.dept_no = d2.dept_no
|
||||
and d1.to_date = '9999-01-01'
|
||||
and d2.to_date = '9999-01-01'
|
||||
and d1.emp_no <> d2.emp_no
|
||||
```
|
||||
|
||||
# 获取所有部门中当前员工薪水最高的相关信息
|
||||
|
||||
```sql
|
||||
select d.dept_no, d.emp_no, MAX(s.salary) as salary
|
||||
from dept_emp as d inner join salaries as s
|
||||
on d.emp_no = s.emp_no
|
||||
and d.to_date = '9999-01-01'
|
||||
and s.to_date = '9999-01-01'
|
||||
group by d.dept_no
|
||||
```
|
||||
|
||||
# 从titles表获取按照title进行分组
|
||||
|
||||
```sql
|
||||
select title, COUNT(*) as t
|
||||
from titles
|
||||
group by title
|
||||
having t >= 2
|
||||
```
|
||||
|
||||
# 从titles表获取按照title进行分组,注意对于重复的emp_no进行忽略。
|
||||
|
||||
```sql
|
||||
select title, COUNT(distinct emp_no) as t
|
||||
from titles
|
||||
group by title
|
||||
having t >= 2
|
||||
```
|
||||
|
||||
# 查找employees表所有emp_no为奇数
|
||||
|
||||
```sql
|
||||
select *
|
||||
from employees
|
||||
where emp_no % 2 = 1 and last_name != 'Mary'
|
||||
order by hire_date desc
|
||||
```
|
||||
|
||||
# 统计出当前各个title类型对应的员工当前薪水对应的平均工资
|
||||
|
||||
```sql
|
||||
select t.title, AVG(s.salary) as avg
|
||||
from titles as t inner join salaries as s
|
||||
on t.emp_no = s.emp_no
|
||||
and t.to_date = '9999-01-01'
|
||||
and s.to_date = '9999-01-01'
|
||||
group by t.title
|
||||
```
|
||||
|
||||
# 获取当前薪水第二多的员工的emp_no以及其对应的薪水salary
|
||||
|
||||
```sql
|
||||
select emp_no, salary
|
||||
from salaries
|
||||
order by salary desc
|
||||
limit 1, 1
|
||||
```
|
||||
|
||||
# 查找当前薪水排名第二多的员工编号emp_no
|
||||
|
||||
```sql
|
||||
select e.emp_no, MAX(s.salary) as salary, e.last_name, e.first_name
|
||||
from employees as e, salaries as s
|
||||
where e.emp_no = s.emp_no
|
||||
and s.to_date = '9999-01-01'
|
||||
and s.salary not in (
|
||||
select MAX(salary)
|
||||
from salaries
|
||||
where s.to_date = '9999-01-01'
|
||||
)
|
||||
```
|
||||
|
||||
# 查找所有员工的last_name和first_name以及对应的dept_name
|
||||
|
||||
查找所有员工的last_name和first_name以及对应的dept_name,也包括暂时没有分配部门的员工
|
||||
|
||||
本题思路为运用两次LEFT JOIN连接嵌套
|
||||
1、第一次LEFT JOIN连接employees表与dept_emp表,得到所有员工的last_name和first_name以及对应的dept_no,也包括暂时没有分配部门的员工
|
||||
2、第二次LEFT JOIN连接上表与departments表,即连接dept_no与dept_name,得到所有员工的last_name和first_name以及对应的dept_name,也包括暂时没有分配部门的员工
|
||||
|
||||
```sql
|
||||
SELECT em.last_name, em.first_name, dp.dept_name
|
||||
FROM (employees AS em LEFT JOIN dept_emp AS de ON em.emp_no = de.emp_no)
|
||||
LEFT JOIN departments AS dp ON de.dept_no = dp.dept_no
|
||||
```
|
||||
|
||||
# 查找员工编号emp_no为10001其自入职以来的薪水salary涨幅值growth
|
||||
|
||||
```sql
|
||||
SELECT (MAX(salary)-MIN(salary)) AS growth
|
||||
FROM salaries WHERE emp_no = '10001'
|
||||
```
|
||||
|
||||
# 查找所有员工自入职以来的薪水涨幅情况
|
||||
|
||||
```sql
|
||||
select a.emp_no, (b.salary - c.salary) as growth
|
||||
from
|
||||
employees as a
|
||||
inner join salaries as b
|
||||
on a.emp_no = b.emp_no and b.to_date = '9999-01-01'
|
||||
inner join salaries as c
|
||||
on a.emp_no = c.emp_no and a.hire_date = c.from_date
|
||||
order by growth asc
|
||||
```
|
||||
|
||||
# 统计各个部门对应员工涨幅的次数总和
|
||||
|
||||
本题关键是要将 每个部门分组,并分别统计工资记录总数,思路如下:
|
||||
1、用INNER JOIN连接dept_emp表和salaries表,并以dept_emp.no分组,统计每个部门所有员工工资的记录总数
|
||||
2、再将上表用INNER JOIN连接departments表,限制条件为两表的dept_no相等,找到dept_no与dept_name的对应关系,最后依次输出dept_no、dept_name、sum
|
||||
|
||||
```sql
|
||||
SELECT de.dept_no, dp.dept_name, COUNT(s.salary) AS sum
|
||||
FROM (dept_emp AS de INNER JOIN salaries AS s ON de.emp_no = s.emp_no)
|
||||
INNER JOIN departments AS dp ON de.dept_no = dp.dept_no
|
||||
GROUP BY de.dept_no
|
||||
```
|
@ -1,702 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 基础
|
||||
|
||||
模式:定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
|
||||
|
||||
主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。
|
||||
|
||||
SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL,各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
|
||||
|
||||
# 查询
|
||||
|
||||
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
|
||||
|
||||
**DISTINCT**
|
||||
|
||||
相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT col1, col2
|
||||
FROM mytable;
|
||||
```
|
||||
|
||||
**LIMIT**
|
||||
|
||||
限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
|
||||
|
||||
返回前 5 行的 SQL:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 0, 5;
|
||||
```
|
||||
|
||||
返回第 3 ~ 5 行:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 2, 3;
|
||||
```
|
||||
|
||||
**注释**
|
||||
|
||||
```sql
|
||||
# 注释
|
||||
SELECT *
|
||||
FROM mytable -- 注释
|
||||
/* 注释1
|
||||
注释2 */
|
||||
```
|
||||
|
||||
# 排序
|
||||
|
||||
**ASC**:升序(默认)
|
||||
**DESC**:降序
|
||||
|
||||
可以按多个列进行排序:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
ORDER BY col1 DESC, col2 ASC;
|
||||
```
|
||||
|
||||
# 过滤
|
||||
|
||||
在应用层也可以过滤数据,但是不在服务器端进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE col IS NULL;
|
||||
```
|
||||
|
||||
下表显示了 WHERE 子句可用的操作符
|
||||
|
||||
| 操作符 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| = < > | 等于 小于 大于 |
|
||||
| <> != | 不等于 |
|
||||
| <= !> | 小于等于 |
|
||||
| >= !< | 大于等于 |
|
||||
| BETWEEN | 在两个值之间 |
|
||||
| IS NULL | 为NULL值 |
|
||||
|
||||
应该注意到,NULL 与 0 、空字符串都不同。
|
||||
|
||||
**AND OR** 用于连接多个过滤条件。优先处理 AND,因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级。
|
||||
|
||||
**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
|
||||
|
||||
**NOT** 操作符用于否定一个条件。
|
||||
|
||||
# 通配符
|
||||
|
||||
通配符也是用在过滤语句中,只能用于文本字段。
|
||||
|
||||
- **%** 匹配 >=0 个任意字符,类似于 \*;
|
||||
|
||||
- **\_** 匹配 ==1 个任意字符,类似于 \.;
|
||||
|
||||
- **[ ]** 可以匹配集合内的字符,用脱字符 ^ 可以对其进行否定
|
||||
|
||||
使用 Like 来进行通配符匹配。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE col LIKE '[^AB]%' -- 不以AB开头的任意文本
|
||||
```
|
||||
|
||||
不要滥用通配符,通配符位于开头处匹配会非常慢。
|
||||
|
||||
# 计算字段
|
||||
|
||||
在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
|
||||
|
||||
计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
|
||||
|
||||
```sql
|
||||
SELECT col1*col2 AS alias
|
||||
FROM mytable
|
||||
```
|
||||
|
||||
**Concat()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
|
||||
|
||||
```sql
|
||||
SELECT Concat(TRIM(col1), ' (', TRIM(col2), ')')
|
||||
FROM mytable
|
||||
```
|
||||
|
||||
# 函数
|
||||
|
||||
各个 DBMS 的函数都是不相同的,因此不可移植。
|
||||
|
||||
## 文本处理
|
||||
|
||||
| 函数 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| LEFT() RIGHT() | 左边或者右边的字符 |
|
||||
| LOWER() UPPER() | 转换为小写或者大写 |
|
||||
| LTRIM() RTIM() | 去除左边或者右边的空格 |
|
||||
| LENGTH() | 长度 |
|
||||
| SUNDEX() | 转换为语音值 |
|
||||
|
||||
其中,**SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE SOUNDEX(col1) = SOUNDEX('apple')
|
||||
```
|
||||
## 日期和时间处理
|
||||
|
||||
日期格式:YYYY-MM-DD
|
||||
|
||||
时间格式:HH:MM:SS
|
||||
|
||||
|函 数 | 说 明|
|
||||
| --- | --- |
|
||||
| AddDate() | 增加一个日期(天、周等)|
|
||||
| AddTime() | 增加一个时间(时、分等)|
|
||||
| CurDate() | 返回当前日期 |
|
||||
| CurTime() | 返回当前时间 |
|
||||
|Date() |返回日期时间的日期部分|
|
||||
|DateDiff() |计算两个日期之差|
|
||||
|Date_Add() |高度灵活的日期运算函数|
|
||||
|Date_Format() |返回一个格式化的日期或时间串|
|
||||
|Day()| 返回一个日期的天数部分|
|
||||
|DayOfWeek() |对于一个日期,返回对应的星期几|
|
||||
|Hour() |返回一个时间的小时部分|
|
||||
|Minute() |返回一个时间的分钟部分|
|
||||
|Month() |返回一个日期的月份部分|
|
||||
|Now() |返回当前日期和时间|
|
||||
|Second() |返回一个时间的秒部分|
|
||||
|Time() |返回一个日期时间的时间部分|
|
||||
|Year() |返回一个日期的年份部分|
|
||||
|
||||
```sql
|
||||
mysql> SELECT NOW();
|
||||
-> '2017-06-28 14:01:52'
|
||||
```
|
||||
|
||||
## 数值处理
|
||||
|
||||
| 函数 | 说明 |
|
||||
| --- | --- |
|
||||
| SIN() | 正弦 |
|
||||
|COS() | 余弦 |
|
||||
| TAN() | 正切 |
|
||||
| ABS() | 绝对值 |
|
||||
| SQRT() | 平方根|
|
||||
| MOD() | 余数|
|
||||
| EXP() | 指数|
|
||||
| PI() | 圆周率|
|
||||
|RAND() | 随机数|
|
||||
|
||||
## 汇总
|
||||
|
||||
|函 数 |说 明|
|
||||
| --- | --- |
|
||||
|AVG() |返回某列的平均值|
|
||||
|COUNT()| 返回某列的行数|
|
||||
|MAX()| 返回某列的最大值|
|
||||
|MIN()| 返回某列的最小值|
|
||||
|SUM() |返回某列值之和|
|
||||
|
||||
AVG() 会忽略 NULL 行。
|
||||
|
||||
DISTINCT 关键字会只汇总不同的值。
|
||||
|
||||
```sql
|
||||
SELECT AVG(DISTINCT col1) AS avg_col
|
||||
FROM mytable
|
||||
```
|
||||
|
||||
# 分组
|
||||
|
||||
分组就是把相同的数据放在同一组中。
|
||||
|
||||
可以对每组数据使用汇总函数进行处理,例如求每组数的平均值等。
|
||||
|
||||
按 col 排序并分组数据:
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
GROUP BY col;
|
||||
```
|
||||
|
||||
WHERE 过滤行,HAVING 过滤分组,行过滤应当先与分组过滤;
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
WHERE col > 2
|
||||
GROUP BY col
|
||||
HAVING COUNT(*) >= 2;
|
||||
```
|
||||
|
||||
GROUP BY 的排序结果为分组字段,而 ORDER BY 也可以以聚集字段来进行排序。
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
GROUP BY col
|
||||
ORDER BY num;
|
||||
```
|
||||
|
||||
分组规定:
|
||||
|
||||
1. GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
|
||||
2. 除了汇总计算语句之外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出;
|
||||
3. NULL 的行会单独分为一组;
|
||||
4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
|
||||
|
||||
# 子查询
|
||||
|
||||
子查询中只能返回一个列。
|
||||
|
||||
可以将子查询的结果作为 WHRER 语句的过滤条件:
|
||||
|
||||
```
|
||||
SELECT *
|
||||
FROM mytable1
|
||||
WHERE col1 IN (SELECT col2
|
||||
FROM mytable2);
|
||||
```
|
||||
|
||||
下面的语句可以检索出客户的订单数量。子查询语句会对检索出的每个客户执行一次:
|
||||
|
||||
```sql
|
||||
SELECT cust_name, (SELECT COUNT(*)
|
||||
FROM Orders
|
||||
WHERE Orders.cust_id = Customers.cust_id)
|
||||
AS orders_num
|
||||
FROM Customers
|
||||
ORDER BY cust_name;
|
||||
```
|
||||
|
||||
# 连接
|
||||
|
||||
连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON。
|
||||
|
||||
连接可以替换子查询,并且比子查询的效率一般会更快。
|
||||
|
||||
可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
|
||||
|
||||
## 内连接
|
||||
|
||||
内连接又称等值连接,使用 INNER JOIN 关键字。
|
||||
|
||||
```
|
||||
select a, b, c
|
||||
from A inner join B
|
||||
on A.key = B.key
|
||||
```
|
||||
|
||||
可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
|
||||
|
||||
```
|
||||
select a, b, c
|
||||
from A, B
|
||||
where A.key = B.key
|
||||
```
|
||||
|
||||
在没有条件语句的情况下返回笛卡尔积。
|
||||
|
||||
## 自连接
|
||||
|
||||
自连接可以看成内连接的一种,只是连接的表是自身而已。
|
||||
|
||||
一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
|
||||
|
||||
**子查询版本**
|
||||
|
||||
```
|
||||
select name
|
||||
from employee
|
||||
where department = (
|
||||
select department
|
||||
from employee
|
||||
where name = "Jim");
|
||||
```
|
||||
|
||||
**自连接版本**
|
||||
|
||||
```
|
||||
select name
|
||||
from employee as e1, employee as e2
|
||||
where e1.department = e2.department
|
||||
and e1.name = "Jim";
|
||||
```
|
||||
|
||||
连接一般比子查询的效率高。
|
||||
|
||||
## 自然连接
|
||||
|
||||
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
|
||||
|
||||
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列;内连接属于自然连接。
|
||||
|
||||
```
|
||||
select *
|
||||
from employee natural join department;
|
||||
```
|
||||
|
||||
## 外连接
|
||||
|
||||
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表的所有行。
|
||||
|
||||
检索所有顾客的订单信息,包括还没有订单信息的顾客。
|
||||
|
||||
```
|
||||
select Customers.cust_id, Orders.order_num
|
||||
from Customers left outer join Orders
|
||||
on Customers.cust_id = Orders.curt_id
|
||||
```
|
||||
|
||||
如果需要统计顾客的订单数,使用聚集函数。
|
||||
|
||||
```
|
||||
select Customers.cust_id,
|
||||
COUNT(Orders.order_num) as num_ord
|
||||
from Customers left outer join Orders
|
||||
on Customers.cust_id = Orders.curt_id
|
||||
group by Customers.cust_id
|
||||
```
|
||||
|
||||
# 组合查询
|
||||
|
||||
使用 **UNION** 来连接两个查询,每个查询必须包含相同的列、表达式或者聚集函数。
|
||||
|
||||
默认会去除相同行,如果需要保留相同行,使用 UNION ALL 。
|
||||
|
||||
只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
|
||||
|
||||
```sql
|
||||
SELECT col
|
||||
FROM mytable
|
||||
WHERE col = 1
|
||||
UNION
|
||||
SELECT col
|
||||
FROM mytable
|
||||
WHERE col =2;
|
||||
```
|
||||
|
||||
# 插入
|
||||
|
||||
**普通插入**
|
||||
|
||||
```sql
|
||||
INSERT INTO mytable(col1, col2)
|
||||
VALUES(val1, val2);
|
||||
```
|
||||
|
||||
**插入检索出来的数据**
|
||||
|
||||
```sql
|
||||
INSERT INTO mytable1(col1, col2)
|
||||
SELECT col1, col2
|
||||
FROM mytable2;
|
||||
```
|
||||
|
||||
**将一个表的内容复制到一个新表**
|
||||
|
||||
```sql
|
||||
CREATE TABLE newtable AS
|
||||
SELECT * FROM mytable;
|
||||
```
|
||||
|
||||
# 更新
|
||||
|
||||
```sql
|
||||
UPDATE mytable
|
||||
SET col = val
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
# 删除
|
||||
|
||||
```sql
|
||||
DELETE FROM mytable
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
|
||||
|
||||
使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
|
||||
|
||||
# 创建表
|
||||
|
||||
```sql
|
||||
CREATE TABLE mytable (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
col1 INT NOT NULL DEFAULT 1,
|
||||
col2 VARCHAR(45) NULL,
|
||||
col3 DATE NULL,
|
||||
PRIMARY KEY (`id`));
|
||||
```
|
||||
|
||||
# 修改表
|
||||
|
||||
**添加列**
|
||||
|
||||
```sql
|
||||
ALTER TABLE mytable
|
||||
ADD col CHAR(20);
|
||||
```
|
||||
|
||||
**删除列**
|
||||
|
||||
```sql
|
||||
ALTER TABLE mytable
|
||||
DROP COLUMN col;
|
||||
```
|
||||
|
||||
**删除表**
|
||||
|
||||
```sql
|
||||
DROP TABLE mytable;
|
||||
```
|
||||
|
||||
# 视图
|
||||
|
||||
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。
|
||||
|
||||
视图具有如下好处:
|
||||
|
||||
1. 简化复杂的 SQL 操作,比如复杂的联结;
|
||||
2. 只使用实际表的一部分数据;
|
||||
3. 通过只给用户访问视图的权限,保证数据的安全性;
|
||||
4. 更改数据格式和表示。
|
||||
|
||||
```sql
|
||||
CREATE VIEW myview AS
|
||||
SELECT Concat(col1, col2) AS concat_col, col3*col4 AS count_col
|
||||
FROM mytable
|
||||
WHERE col5 = val;
|
||||
```
|
||||
|
||||
# 存储过程
|
||||
|
||||
存储过程可以看成是对一系列 SQL 操作的批处理;
|
||||
|
||||
**使用存储过程的好处**
|
||||
|
||||
1. 把实现封装在了存储过程中,不仅简单,也保证了安全性;
|
||||
2. 可以复用代码;
|
||||
3. 由于是预先编译,因此具有很高的性能。
|
||||
|
||||
**创建存储过程**
|
||||
|
||||
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
|
||||
|
||||
包含 in、out 和 inout 三种参数。
|
||||
|
||||
给变量赋值都需要用 select into 语句。
|
||||
|
||||
每次只能给一个变量赋值,不支持集合的操作。
|
||||
|
||||
```sql
|
||||
delimiter //
|
||||
|
||||
create procedure myprocedure( out ret int )
|
||||
begin
|
||||
declare y int;
|
||||
select sum(col1)
|
||||
from mytable
|
||||
into y;
|
||||
select y*y into ret;
|
||||
end //
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
```sql
|
||||
call myprocedure(@ret);
|
||||
select @ret;
|
||||
```
|
||||
|
||||
# 游标
|
||||
|
||||
在存储过程中使用游标可以对一个结果集进行移动遍历。
|
||||
|
||||
游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
|
||||
|
||||
**使用游标的四个步骤:**
|
||||
|
||||
1. 声明游标,这个过程没有实际检索出数据;
|
||||
2. 打开游标;
|
||||
3. 取出数据;
|
||||
4. 关闭游标;
|
||||
|
||||
```sql
|
||||
delimiter //
|
||||
create procedure myprocedure(out ret int)
|
||||
begin
|
||||
declare done boolean default 0;
|
||||
|
||||
declare mycursor cursor for
|
||||
select col1 from mytable;
|
||||
# 定义了一个continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
|
||||
declare continue handler for sqlstate '02000' set done = 1;
|
||||
|
||||
open mycursor;
|
||||
|
||||
repeat
|
||||
fetch mycursor into ret;
|
||||
select ret;
|
||||
until done end repeat;
|
||||
|
||||
close mycursor;
|
||||
end //
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
# 触发器
|
||||
|
||||
触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE
|
||||
|
||||
触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化。
|
||||
|
||||
INSERT 触发器包含一个名为 NEW 的虚拟表。
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER mytrigger AFTER INSERT ON mytable
|
||||
FOR EACH ROW SELECT NEW.col;
|
||||
```
|
||||
|
||||
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
|
||||
|
||||
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。
|
||||
|
||||
可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。
|
||||
|
||||
MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。
|
||||
|
||||
# 事务处理
|
||||
|
||||
**基本术语**
|
||||
|
||||
1. 事务(transaction)指一组 SQL 语句;
|
||||
2. 回退(rollback)指撤销指定 SQL 语句的过程;
|
||||
3. 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
|
||||
4. 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
|
||||
|
||||
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CRETE 和 DROP 语句。
|
||||
|
||||
MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就会提交一次。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
|
||||
|
||||
通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。
|
||||
|
||||
如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
|
||||
|
||||
```sql
|
||||
START TRANSACTION
|
||||
// ...
|
||||
SAVEPOINT delete1
|
||||
// ...
|
||||
ROLLBACK TO delete1
|
||||
// ...
|
||||
COMMIT
|
||||
```
|
||||
|
||||
# 字符集
|
||||
|
||||
**基本术语**
|
||||
|
||||
1. 字符集为字母和符号的集合;
|
||||
2. 编码为某个字符集成员的内部表示;
|
||||
3. 校对字符指定如何比较,主要用于排序和分组。
|
||||
|
||||
除了给表指定字符集和校对外,也可以给列指定:
|
||||
|
||||
```sql
|
||||
CREATE TABLE mytable
|
||||
(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
|
||||
DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
|
||||
```
|
||||
|
||||
可以在排序、分组时指定校对:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
ORDER BY col COLLATE latin1_general_ci;
|
||||
```
|
||||
|
||||
# 权限管理
|
||||
|
||||
MySQL 的账户信息保存在 mysql 这个数据库中。
|
||||
|
||||
```sql
|
||||
USE mysql;
|
||||
SELECT user FROM user;
|
||||
```
|
||||
|
||||
**创建账户**
|
||||
|
||||
```sql
|
||||
CREATE USER myuser IDENTIFIED BY 'mypassword';
|
||||
```
|
||||
|
||||
新创建的账户没有任何权限。
|
||||
|
||||
**修改账户名**
|
||||
|
||||
```sql
|
||||
RENAME myuser TO newuser;
|
||||
```
|
||||
|
||||
**删除账户**
|
||||
|
||||
```sql
|
||||
DROP USER myuser;
|
||||
```
|
||||
|
||||
**查看权限**
|
||||
|
||||
```sql
|
||||
SHOW GRANTS FOR myuser;
|
||||
```
|
||||

|
||||
|
||||
账户用 username@host 的形式定义,username@% 使用的是默认主机名。
|
||||
|
||||
**授予权限**
|
||||
|
||||
```sql
|
||||
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
|
||||
```
|
||||
|
||||
**删除权限**
|
||||
|
||||
```sql
|
||||
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
|
||||
```
|
||||
|
||||
GRANT 和 REVOKE 可在几个层次上控制访问权限:
|
||||
|
||||
- 整个服务器,使用 GRANT ALL和 REVOKE ALL;
|
||||
- 整个数据库,使用 ON database.\*;
|
||||
- 特定的表,使用 ON database.table;
|
||||
- 特定的列;
|
||||
- 特定的存储过程。
|
||||
|
||||
**更改密码**
|
||||
|
||||
必须使用 Password() 函数
|
||||
|
||||
```sql
|
||||
SET PASSWROD FOR myuser = Password('newpassword');
|
||||
```
|
||||
|
@ -1,219 +0,0 @@
|
||||
[TOC]
|
||||
# 两阶段提交协议(2PC)
|
||||
|
||||
保证多个节点操作的原子性,实现事务操作。
|
||||
|
||||
包含两类节点:协调者(coordinator)和参与者(participants),协调者只有一个,参与者可以有多个。
|
||||
|
||||
**Phase1:**请求阶段,协调者通知事务参与者准备提交或者取消事务,然后进入表决过程。在表决过程中,参与者告知协调者自己的决策:如果事务在本地执行成功,就告知同意,否者告知取消。
|
||||
|
||||
**Phase2:**提交阶段,协调者将基于表决阶段的投票结果进行决策,当且仅当所有参与者同意提交事务,决策结果才为同意,否者为取消。协调者把决策结果通知所有的参与者,参与者接收到协调者发来的消息后执行相应的操作。
|
||||
|
||||
# Paxos协议
|
||||
|
||||
Paxos用于确保多个节点对某个投票达成一致。常用来选举主节点,当主节点出现故障时,使用Paxos协议就可以从备节点中选举出新的主节点。也用来构建高可用的全局服务,例如分布式锁服务,全局命名和配置服务等,Apache Zookeeper就实现了Paxos。
|
||||
|
||||
Paxos协议涉及两类节点:提议者(proposer)和接受者(acceptor)。
|
||||
|
||||
在只有一个proposer的情况下,Paxos协议执行步骤如下:
|
||||
|
||||
1. 批准:proposer发送提议给acceptor,acceptor决定接受或者拒绝这个提议;
|
||||
2. 确认:如果超过一半的acceptor接受,则提议生效,proposer发送acknowledge消息通知所有的acceptor提议生效。
|
||||
|
||||
如果存在网络分区的情况下,可能会存在多个proposer,用提议号来控制每个提议,只有提议号最大的才会被接受。
|
||||
|
||||

|
||||
|
||||
# Raft协议
|
||||
|
||||
Raft和Poxas同为一致性协议,但是更容易理解,也更容易实现。
|
||||
|
||||
[Raft可视化](http://thesecretlivesofdata.com/raft/)
|
||||
|
||||
有三种节点:Follower、Candidate和Leader。
|
||||
|
||||
Leader会周期性的发送消息给Follower。每个Follower都设置了一个随机的竞选超时时间,一般为150ms~300ms,如果在这个时间内没有收到Leader的消息,则节点就会变成Candidate,进入竞选阶段。最开始系统只有Follower。
|
||||
|
||||

|
||||
|
||||
Candidate会请求其它所有节点的投票,如果一个Candidate获得多数票,则它成为Leader。
|
||||
|
||||
所有来自客户端的修改都会被传到Leader,每个修改被作为一个entry添加到节点的日志。Leader先不会修改其节点值,而是把entry都赋值到所有的Follower,只有所有的Follower都写入这个entry,Leader才会修改节点值,并且通知所有的Follower也修改节点值。
|
||||
|
||||

|
||||
|
||||
该协议也可以用来处理网络分区的问题。
|
||||
|
||||

|
||||
|
||||
# 拜占庭将军问题
|
||||
|
||||
该问题主要用于保证分布式系统的一致性和可用性。
|
||||
|
||||
## 问题场景
|
||||
|
||||
拜占庭帝国想要进攻一个强大的敌人,为此派出了10支军队去包围这个敌人。这个敌人虽不比拜占庭帝国,但也足以抵御5支常规拜占庭军队的同时袭击。基于一些原因,这10支军队不能集合在一起单点突破,必须在分开的包围状态下同时攻击。他们任一支军队单独进攻都毫无胜算,除非有至少6支军队同时袭击才能攻下敌国。他们分散在敌国的四周,依靠通信兵相互通信来协商进攻意向及进攻时间。困扰这些将军的问题是,他们不确定他们中是否有叛徒,叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们能否找到一种分布式的协议来让他们能够远程协商,从而赢取战斗?这就是著名的拜占庭将军问题。
|
||||
|
||||
## 相关问题:两军问题
|
||||
|
||||
拜占庭将军问题中并不去考虑通信兵是否会被截获或无法传达信息等问题,即消息传递的信道绝无问题。如果需要考虑信道是有问题的,这涉及到了另一个相关问题:两军问题。
|
||||
|
||||

|
||||
|
||||
白军驻扎在沟渠里,蓝军则分散在沟渠两边。白军比任何一支蓝军都更为强大,但是蓝军若能同时合力进攻则能够打败白军。他们不能够远程的沟通,只能派遣通信兵穿过沟渠去通知对方蓝军协商进攻时间。是否存在一个能使蓝军必胜的通信协议,这就是两军问题。通信兵得经过敌人的沟渠,在这过程中他可能被捕,也就是说,两军问题中信道是不可靠的,并且其中没有叛徒之说,这就是两军问题和拜占庭将军问题的根本性不同。
|
||||
|
||||
倘若1号蓝军(简称1)向2号蓝军(简称2)派出了通信兵,若1要知道2是否收到了自己的信息,1必须要求2给自己传输一个回执,说“你的信息我已经收到了,我同意你提议的明天早上10点9分准时进攻”。然而,就算2已经送出了这条信息,2也不能确定1就一定会在这个时间进攻,因为2发出的回执1并不一定能够收到。所以,1必须再给2发出一个回执说“我收到了”,但是1也不会知道2是否收到了这样一个回执,所以1还会期待一个2的回执。但在这个系统中永远需要存在一个回执,这对于两方来说都并不一定能够达成十足的确信。更要命的是,我们还没有考虑,通信兵的信息还有可能被篡改。由此可见,经典情形下两军问题是不可解的,并不存在一个能使蓝军一定胜利的通信协议。
|
||||
|
||||
不幸的是,两军问题作为现代通信系统中必须解决的问题,我们尚不能将之完全解决,这意味着你我传输信息时仍然可能出现丢失、监听或篡改的情况。但我们能不能通过一种相对可靠的方式来解决大部分情形呢?这需要谈到TCP协议。
|
||||
|
||||

|
||||
|
||||
TCP协议中,A先向B发出一个随机数x,B收到x了以后,发给A另一个随机数y以及x+1作为答复,这样A就知道B已经收到了,因为要破解随机数x可能性并不大;然后A再发回y+1给B,这样B就知道A已经收到了。这样,A和B之间就建立一个可靠的连接,彼此相信对方已经收到并确认了信息。而事实上,A并不会知道B是否收到了y+1;并且,由于信道的不可靠性,x或者y都是可能被截获的,这些问题说明了即使是三次握手,也并不能够彻底解决两军问题,只是在现实成本可控的条件下,我们把TCP协议当作了两军问题的现实可解方法。
|
||||
|
||||
## 问题形式化
|
||||
|
||||
只要忠诚的将军能够让别的将军接受到自己真实意图,并且所有忠诚将军能够达成一致的决定,就能解决该问题。
|
||||
|
||||
定义一个变量vi,作为其他将军收到的第i个将军的命令值;之后,定义一个函数来处理向量(v1,v2,…,vn),各将军用这个函数的结果作为自己最终采用的命令。
|
||||
|
||||
**一致性条件**:每一个忠诚的将军必须得到相同的(v1,v2,…,vn)。这意味着,忠诚的将军并不一定使用i将军送来的信息作为vi,i将军也可能是叛徒。
|
||||
|
||||
**正确性条件**:若i将军是忠诚的,其他忠诚的将军必须以他送出的值作为vi。
|
||||
|
||||
改写一致性条件如下:无论i将军是忠诚或是叛徒,任何两个忠诚的将军都使用相同的vi。这是很巧妙的一步转换,如此一致性条件)和正确性条件都只涉及一个将军i如何帮别的将军接受自己送出的值vi,所以可以将问题改为司令-副官模式来简化问题,即一个司令把自己的命令传递给n-1个副官。
|
||||
|
||||
**IC1:**所有忠诚的副官遵守一个命令,即一致性。
|
||||
|
||||
**IC2:**若司令是忠诚的,每一个忠诚的副官遵守他发出的命令,即正确性。
|
||||
|
||||
司令-副官模式只要将司令遍历各个将军,就可以变成完整问题,而他们采用的算法可以是完全一致的。在这种模式下,司令副官的形式下达成的一致意味着司令的命令得到了有效传达,若出现了异议,有异议的将军会作为司令发起新的司令副官模式寻求自己的观点表达,以协商达成一致。
|
||||
|
||||
有两种解决方法:口头协议和书面协议。
|
||||
|
||||
**4.4 口头协议**
|
||||
|
||||
口头协议满足以下条件:
|
||||
|
||||
**A1:**每个被发送的消息都能够被正确的投递
|
||||
|
||||
**A2:**信息接收者知道是谁发送的消息
|
||||
|
||||
**A3:**能够知道缺少的消息
|
||||
|
||||
**4.4.1 OM(m)**
|
||||
|
||||
采用口头协议,若叛徒数少于1/3,则拜占庭将军问题可解。这个结论说明了,对于拜占庭将军问题,由于叛徒可以做出各种各样的判断,就必须四模冗余系统才足够容错。
|
||||
|
||||
**OM(0)算法**
|
||||
|
||||
**(1)**司令将他的命令发送给每个副官。
|
||||
|
||||
**(2)**每个副官采用从司令发来的命令;如果没有收到命令,则默认为撤退命令。
|
||||
|
||||
**OM(m)算法**
|
||||
|
||||
**(1)**司令将他的命令发送给每个副官。
|
||||
|
||||
**(2)**对于每个i,vi是每个副官i从司令收到的命令,如果没有收到命令,则默认为撤退命令。副官i在OM(m-1) 中作为发令者将之发送给另外n-2 个副官。
|
||||
|
||||
**(3)**对于每个i,和每个j ≠ i,vj 是副官i 从第2步中的副官j (使用OM(m-1)算法)发送过来的命令,如果没有收到第2步中副官j 的命令,则默认为撤退命令。最后副官i 使用majority(v1,…,vn-1)得到命令。
|
||||
|
||||
这个算法是一个递归算法,在OM(m)中需要采用OM(m-1)得到相关结果。m代表的是叛徒数量,从m到0,意味着对于每个将军,需要m+1轮的算法才能完成。
|
||||
|
||||
该算法是关于m的,意味着使用该算法必须知道有多少个叛徒。
|
||||
|
||||
对于任意k<m,在第m-k+1步中OM(k)的vi,都是利用OM(k-1)得来的,即每个将军将会在OM(k-1)中询问其他人,i将军传给他们的是什么,而其他人传递回来的信息则是利用OM(k-2)得到。
|
||||
|
||||
**4.4.2 实例**
|
||||
|
||||
**1. m=1,n=3**
|
||||
|
||||
首先考虑司令忠诚而副官2是叛徒的情况,司令把进攻命令传给了两个副官1和副官2,但是由于副官2为了不让他们达成一致,将司令的命令改成了撤退。那对于副官1来说,他应该如何判断?他无法知道是司令是叛徒还是副官2是叛徒,从而无法判断。
|
||||
|
||||

|
||||
而如果司令是叛徒,两个副官忠诚,司令会发送两个不同的命令。当两个副官对照命令时,他们又凌乱了,无法判断司令是叛徒或者对方是叛徒,从而又无法判断。这个情形非常简易的说明了三模冗余是无法动态容错的。
|
||||
|
||||

|
||||
|
||||
**2. m=1,n=4**
|
||||
|
||||
首先考虑司令忠诚而副官3是叛徒的情况,倘若司令在OM(1)中给各副官发送的消息都是进攻(A),之后OM(0)时,叛徒副官3给副官1和副官2说他收到的消息是撤退(R)。那么对于副官1(或副官2)来说,他综合司令、副官3和副官2(或副官1)后得到的消息向量都将会是(A,A,R),利用majority函数之后,将会采用A。
|
||||
|
||||

|
||||
|
||||
倘若司令是叛徒,那么我们已经不需要满足IC2。为方便,我们假设叛徒司令在OM(1)会给三个副官发送的信息是(x,y,z),其中x,y,z都可以是A或R的任意一种。之后,三位忠诚的副官将会按照OM(0)要求的那样,交换他们收到的信息。对于副官1,他综合司令、副官2和副官3后得到的消息向量将会是(x,y,z),可以发现对于其他两个忠实的副官,他们得到的消息向量也将是(x,y,z)。不管x,y,z如何变化,majority(x,y,z)对于三人来说都是一样的,所以三个副官将会采用一致的行动。
|
||||
|
||||
**3. m=2,n=7**
|
||||
|
||||
我们先讨论司令忠诚的情形,假设叛徒为副官5和副官6。在OM(2)中,司令将攻击命令(A)传给各个副官。在OM(1)中,忠诚的副官们将会发送他们收到的消息(A),但由于副官5和副官6是叛徒,他们将会发送别的信息(比如R)。
|
||||
|
||||

|
||||
|
||||
对于副官1,他收到了司令传来的命令,他会直接采用majority函数综合司令和其他将军传来的信息吗?他不会,因为这还在OM(1)中,他并不知道司令是不是叛徒,他会利用询问别人的方式来确认将军的命令,但是按照算法他会把司令的命令作为v1(即v1=A)并传给其他人。接下来他会努力取得其他的v2~v6的值,这时已经在OM(1)中了,副官1绝不会轻易相信别人传来的消息,比如副官2给他传来了命令A,但是他会怀疑副官2传来的消息,所以他用OM(1)大法,问其他人副官2传给了他们什么,副官3和副官4诚实的告诉副官1,副官2给他们传的是A,而这时副官5和副官6又要撒谎了,他们又乱说,我们姑且假定他们传来的是x’和y’吧。这样,终于进入到了OM(0),这时副官1将会综合其他副官对于v2的反馈,得到向量(A,A,A,x’,y’),再利用majority函数,得到了v2=A。如下图,
|
||||
|
||||

|
||||
|
||||
我们就可以得到副官1的v1~v6向量为(A,A,A,A,x,y),利用majority函数,副官1最终采用的行动会是A。 类似的,我们可以发现,其他几个忠诚的副官得到的命令向量都会是(A,A,A,A,x,y),利用majority函数后采用的行动都会是A。
|
||||
|
||||
**4.5 书面协议**
|
||||
|
||||
除了A1,A2和A3以外,我们在口头协议之上添加一个条件A4,使之成为书面协议
|
||||
|
||||
**A4**:(a)签名不可伪造,一旦被篡改即可发现,而叛徒的签名可被其他叛徒伪造;(b)任何人都可以验证签名的可靠性。
|
||||
|
||||
**4.5.1 SM(m)**
|
||||
|
||||
不管将军总数n和叛徒数量m,只要采用该算法,忠诚的将军总能达到一致.用集合Vi来表示i副官收到的命令集,这是一个集合,也就是满足互异性(没有重复的元素)等集合的条件。类似的,我们定义choice(V)函数来决定各个副官的选择,这个函数可以有非常多种形式,他只要满足了以下两个条件:
|
||||
|
||||
**(1)**如果集合V只包含了一个元素v,那么choice(V)=v
|
||||
|
||||
**(2)**choice(o)=RETREAT,其中o是空集
|
||||
|
||||
SM(m)算法并不是一个递归算法,我们只要让各个副官收到的V集相同,choice(V)也一定能够得到相同的值。
|
||||
|
||||
初始化Vi=空集合。
|
||||
|
||||
**(1)**将军签署命令并发给每个副官;
|
||||
|
||||
**(2)**对于每个副官i:
|
||||
|
||||
(2.1)如果副官i从发令者收到v:0的消息,且还没有收到其他命令序列,那么他
|
||||
|
||||
(2.1.1)使Vi为{v};
|
||||
|
||||
(2.1.2)发送v:0:i给其他所有副官。
|
||||
|
||||
(2.2)如果副官i收到了形如v:0:j1:…:jk的消息且v不在集合Vi中,那么他
|
||||
|
||||
(2.2.1)添加v到Vi;
|
||||
|
||||
(2.2.2)如果k<m,那么发送v:0:j1:…:jk:i 给每个不在j1,..,jk 中的副官。
|
||||
|
||||
**(3)**对于每个副官i,当他不再收到任何消息,则遵守命令choive(Vi)。
|
||||
|
||||
值得注意的是,如果司令忠诚,由于其签名不可伪造,所有忠诚的副官都将得到一个单点集{v},他们采用的命令集Vi相同,得到的choive(Vi)也为v,满足了IC1和IC2。如果司令并非忠诚,只需要满足IC1,但是算法SM(m)使得所有忠诚的副官得到相同的Vi,使用choice()函数后采用的命令也就一定相同。
|
||||
|
||||
**4.5.2 实例**
|
||||
|
||||
司令是叛徒的状况稍难想象,举个例子,n=3,m=1,其中司令是叛徒,这是口头协议不能解决的状况。
|
||||
|
||||

|
||||
|
||||
很显然,副官1得到的V1={A,R},副官2得到相同的V2={A,R}。他们采用choice函数后得到的命令一定相同。
|
||||
|
||||
相似的,n=4,m=2,其中司令是叛徒,这同样是口头协议不能解决的状况。
|
||||
|
||||

|
||||
|
||||
副官1和副官2得到的V1=V2={A,R},他们采用choice函数后得到的命令也相同。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
|
||||
- [简单解释 MapReduce 算法](http://www.cnblogs.com/wuyudong/p/mapreduce-principle.html)
|
||||
- [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun)
|
||||
- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)
|
||||
- [如何浅显易懂地解说 Paxos 的算法?](https://www.zhihu.com/question/19787937/answer/26584242)
|
||||
- [图解 Paxos 一致性协议](http://blog.jobbole.com/106327/?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com)
|
||||
- [raft-zh_cn](https://github.com/maemual/raft-zh_cn)
|
||||
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
|
@ -1,116 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 基本概念
|
||||
|
||||
## 异常
|
||||
|
||||
### 服务器宕机
|
||||
|
||||
服务器宕机会导致节点失去所有内存信息,设计存储系统时需要考虑如何通过读取持久化介质中的数据来恢复内存信息。
|
||||
|
||||
### 网络异常
|
||||
|
||||
有一种特殊的网络异常称为“网络分区”,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
|
||||
|
||||
设计容错系统的一个基本原则:总是假设网络将会出现异常并采取相应的处理措施。
|
||||
|
||||
### 磁盘故障
|
||||
|
||||
磁盘故障是一种发生概率很高的异常,分布式存储系统需要使用冗余机制,将数据存储到多台服务器,即使其中一台服务器磁盘出现故障,也能从其他服务器上恢复数据。
|
||||
|
||||
## 衡量指标
|
||||
|
||||
### 性能
|
||||
|
||||
常见的性能指标有:吞吐能力、响应时间。
|
||||
|
||||
其中,吞吐能力指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。
|
||||
|
||||
这两个指标往往是 **矛盾** 的,追求高吞吐的系统,往往很难做到低延迟;同理,追求低延迟也很难做到高吞吐。
|
||||
|
||||
### 可用性
|
||||
|
||||
可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,例如 4 个 9 位 99.99%。
|
||||
|
||||
### 一致性
|
||||
|
||||
分布式系统采用冗余机制保存数据的多份副本,如何保证副本之间的一致性是整个分布式系统的核心问题。
|
||||
|
||||
### 可扩展性
|
||||
|
||||
指系统通过扩展集群服务器规模来提高性能的能力.理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线程增加。
|
||||
|
||||
# 数据分布
|
||||
|
||||
分布式存储系统中的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。
|
||||
|
||||
将数据分布在多台机器时,需要考虑到多台机器的负载均衡。衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。分布式存储系统需要能够自动识别负载高的节点,当某个节点的负载较高,就将它的部分数据迁移到其它节点,实现自动负载均衡。
|
||||
|
||||
## 哈希分布
|
||||
|
||||
哈希分布就是将数据计算 hash 值之后,按照 hash 值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。
|
||||
|
||||
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值改变,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
|
||||
|
||||
### 一致性哈希
|
||||
|
||||
Distributed Hash Table,DHT:对于哈希空间 0~2<sup>n</sup>,将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
|
||||
|
||||

|
||||
|
||||
一致性哈希的优点是在加入或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,而对于节点 A、B、D 都没有影响。
|
||||
|
||||

|
||||
|
||||
## 顺序分布
|
||||
|
||||
顺序分布将有序的数据划分为多个连续的部分,按一定策略分布到不同节点上。
|
||||
|
||||
## 负载均衡
|
||||
|
||||
分布式存储系统中每个集群都有一个总控节点,其它节点为工作节点,由总控节点根据全局负载信息进行整体调度。系统运行过程中需要不断地执行数据迁移工作,将数据从负载较高的工作节点迁移到负载较低的工作节点。
|
||||
|
||||
工作节点通过心跳包(Heartbeat,定时发送)将节点负载相关的信息,如 CPU、内存、磁盘、网络等资源使用情况发送给总控节点。
|
||||
|
||||
考虑到一个新上线的节点由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。
|
||||
|
||||
# 复制
|
||||
|
||||
## 主从复制
|
||||
|
||||
分布式存储系统采用冗余机制,将一份数据保存多份副本。多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,并将写操作日志同步到备副本,备副本通过回放操作日志来保证数据一致性。主副本和备副本都可以处理读请求。当主副本出现故障时,可以将一个备副本选举为主副本。
|
||||
|
||||
有两种同步方式:强同步复制和异步复制。强同步复制要求备副本对写操作日志回放成功之后才算完成,而异步复制只需要主副本在本地上的修改完成就算完成。强同步复制一致性好,但是可用性差,异步复制方式相反。
|
||||
|
||||
## CAP
|
||||
|
||||
分布式存储系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition),设计中往往需要弱化对某个特性的保证。
|
||||
|
||||
- 一致性:读操作总是能读取到之前完成的写操作结果,满足这个条件的系统称为强一致性系统;
|
||||
- 可用性:读写操作在单台机器发生故障的情况下仍然能够正常执行;
|
||||
- 分区容忍性:允许发生网络分区。
|
||||
|
||||
# 容错
|
||||
|
||||
大规模集群每天都有故障发生,分布式存储系统需要实现自动化容错,才能实现高可用以及减少人工运维成本。
|
||||
|
||||
## 故障检测
|
||||
|
||||
通过**租约机制**来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。当节点 B 拥有的租约规定的期限块到达时,节点 B 向 A 重新申请租约,否则主动停止服务。
|
||||
|
||||
## 故障恢复
|
||||
|
||||
当某个节点故障时,就将它上面的服务迁移到其它节点。
|
||||
|
||||
# CDN 架构
|
||||
|
||||
CND 通过将网络内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。这样既可以减轻服务器的负担,也可以提高用户的访问速度。
|
||||
|
||||
从下图可以看出,DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。
|
||||
|
||||

|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
|
File diff suppressed because it is too large
Load Diff
@ -1,163 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 正在完成...
|
||||
|
||||
# 解题思路
|
||||
|
||||
空间限制问题基本思路是拆分。如果一个数据很大,无法放在一台机器上,就用分布式的方法放到多台机器上;在程序运行时无法直接加载一个大文件,就分多次加载,并且每次运行结果都保存到一个小文件中。
|
||||
|
||||
|
||||
|
||||
# 布隆过滤器
|
||||
|
||||
**题目**
|
||||
|
||||
不安全网页的黑名单包含 100 亿个黑名单网页,每个网页的 URL 最多占用 64B。现在想要实现一种网页过滤系统,可以根据网页的 URL 判断该网页是否在黑名单上,请设计该系统。
|
||||
|
||||
**要求**
|
||||
|
||||
1. 该系统允许有万分之一以下的判断失误率。
|
||||
2. 使用的额外空间不要超过 30GB。
|
||||
|
||||

|
||||
|
||||
[程序员代码面试指南 P303]()
|
||||
|
||||
# 查找所有包含某一组词的文件
|
||||
|
||||
> 给定数百万份文件,设计一个程序找出所有包含某一组词的文件,该程序会被多次调用。
|
||||
|
||||
## 1. 预处理
|
||||
|
||||
预处理每个文件,并创建一个散列表的索引。这个散列表会将词映射到含有这个
|
||||
词的一组文件。
|
||||
|
||||
```html
|
||||
“books” -> {doc2, doc3, doc6, doc8}
|
||||
“many” -> {doc1, doc3, doc7, doc8, doc9}
|
||||
```
|
||||
|
||||
若要查找“many books”,只需对“books”和“many”的值进行交集运算,于是得到结果{doc3, doc8}。
|
||||
|
||||
## 2. 拆分
|
||||
|
||||
若有数百万份文件,那么一台机器无法放下这么多文件,需要存放到多台机器上。并且散列表可能也会很大,一台机器可能无法放下整个散列表,因此需要将散列表拆分到多台机器上。
|
||||
|
||||
拆分的方法有很多,例如根据键的散列值,或者根据键的范围。
|
||||
|
||||
## 3. 查找
|
||||
|
||||
例如查找“after builds boat amaze banana”,先划分成单词序列,然后每个单词都到存放该单词散列表的机器上去查找,最后将每个单词的查找结果做交集运算。
|
||||
|
||||
# 大型社交网站计算两个人的好友关系
|
||||
|
||||
> 你会如何设计诸如Facebook或LinkedIn的超大型社交网站?请设计一种算法,展示两个人之间的“连接关系”或“社交路径”(比如,我 → 鲍勃 → 苏珊 → 杰森 → 你)
|
||||
|
||||
## 1. 好友关系的实现
|
||||
|
||||
为每个用户保存好友 id 的列表,利用这个列表,可以构建一个图,每个用户看作一个结点,两个结点之间若有连线,则表示这两个用户为朋友。
|
||||
|
||||
要找到两个人之间的连接,可以从其中一个人开始,直接进行广度优先搜索。为什么深度优先搜索效果不彰呢?因为它非常低效。两个用户可能只有一度之隔,却可能要在他们的“子树”中搜索几百万个结点后,才能找到这条非常简单而直接的连接。
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
private ArrayList<Integer> friendIDs;
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 进行查找
|
||||
|
||||
处理LinkedIn或Facebook这种规模的服务时,不可能将所有数据存放在一台机器上。这就意味着前面定义的简单数据结构Person并不管用,朋友的资料和我们的资料不一定在同一台机器上。
|
||||
|
||||
1. 针对每个朋友ID,找出所在机器的位置:int machine_index = getMachineIDForUser(personID);
|
||||
2. 转到编号为#machine_index的机器。
|
||||
3. 在那台机器上,执行:Person friend = getPersonWithID(person_id);。
|
||||
|
||||
下面的代码描绘了这一过程。我们定义了一个Server类,包含一份所有机器的列表,还有一个Machine类,代表一台单独的机器。这两个类都用了散列表,从而有效地查找数据。
|
||||
|
||||
```java
|
||||
public class Server {
|
||||
HashMap<Integer, Machine> machines = new HashMap<Integer, Machine>();
|
||||
HashMap<Integer, Integer> personToMachineMap = new HashMap<Integer, Integer>();
|
||||
|
||||
public Machine getMachineWithId(int machineID) {
|
||||
return machines.get(machineID);
|
||||
}
|
||||
|
||||
public int getMachineIDForUser(int personID) {
|
||||
Integer machineID = personToMachineMap.get(personID);
|
||||
return machineID == null ? -1 : machineID;
|
||||
}
|
||||
|
||||
public Person getPersonWithID(int personID) {
|
||||
Integer machineID = personToMachineMap.get(personID);
|
||||
if (machineID == null) {
|
||||
return null;
|
||||
}
|
||||
Machine machine = getMachineWithId(machineID);
|
||||
if (machine == null) {
|
||||
return null;
|
||||
}
|
||||
return machine.getPersonWithID(personID);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
private ArrayList<Integer> friends;
|
||||
private int personID;
|
||||
private String info;
|
||||
|
||||
public String getInfo() { return info; }
|
||||
public void setInfo(String info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public int[] getFriends() {
|
||||
int[] temp = new int[friends.size()];
|
||||
for (int i = 0; i < temp.length; i++) {
|
||||
temp[i] = friends.get(i);
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
public int getID() { return personID; }
|
||||
public void addFriend(int id) { friends.add(id); }
|
||||
|
||||
public Person(int id) {
|
||||
this.personID = id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Machine {
|
||||
public HashMap<Integer, Person> persons = new HashMap<Integer, Person>();
|
||||
public int machineID;
|
||||
|
||||
public Person getPersonWithID(int personID) {
|
||||
return persons.get(personID);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 优化
|
||||
|
||||
**减少机器间跳转的次数**
|
||||
|
||||
从一台机器跳转到另一台机器的开销很昂贵。不要为了找到某个朋友就在机器之间任意跳转,而是试着批处理这些跳转动作。举例来说,如果有五个朋友都在同一台机器上,那就应该一次性找出来。
|
||||
|
||||
**智能划分用户和机器**
|
||||
|
||||
人们跟生活在同一国家的人成为朋友的可能性比较大。因此,不要随意将用户划分到不同机器上,而应该尽量按国家、城市、州等进行划分。这样一来,就可以减少跳转的次数。
|
||||
|
||||
**广度优先搜索通常要求“标记”访问过的结点。在这种情况下你会怎么做?**
|
||||
|
||||
在广度优先搜索中,通常我们会设定结点类的visited标志,以标记访问过的结点。但针对此题,我们并不想这么做。同一时间可能会执行很多搜索操作,因此直接编辑数据的做法并不妥当。反之,我们可以利用散列表模仿结点的标记动作,以查询结点id,看它是否访问过。
|
||||
|
||||
## 4. 其它扩展问题
|
||||
|
||||
1. 在真实世界中,服务器会出故障。这会对你造成什么影响?
|
||||
2. 你会如何利用缓存?
|
||||
3. 你会一直搜索,直到图的终点(无限)吗?该如何判断何时放弃?
|
||||
4. 在现实生活中,有些人比其他人拥有更多朋友的朋友,因此更容易在你和其他人之间构建一条路径。该如何利用该数据选择从哪里开始遍历?
|
@ -1,301 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 事务四大特性
|
||||
|
||||
## 原子性
|
||||
|
||||
要么都执行,要么都不执行。
|
||||
|
||||
## 一致性
|
||||
|
||||
事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
|
||||
|
||||
## 隔离性
|
||||
|
||||
多个事务单独执行,互不影响。
|
||||
|
||||
## 持久性
|
||||
|
||||
即使系统发生故障,事务执行的结果也不能丢失。持久性通过数据库备份和恢复来保证。
|
||||
|
||||
# 数据不一致
|
||||
|
||||
## 丢失修改
|
||||
|
||||
T<sub>1</sub> 和 T<sub>2</sub> 两个事务同时对一个数据进行修改,T<sub>1</sub> 先修改,T<sub>2</sub> 随后修改,T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
|
||||
|
||||
## 读脏数据
|
||||
|
||||
T<sub>1</sub> 做修改后写入数据库,T<sub>2</sub> 读取这个修改后的数据,但是如果 T<sub>1</sub> 撤销了这次修改,使得 T<sub>2</sub> 读取的数据是脏数据。
|
||||
|
||||
## 不可重复读
|
||||
|
||||
T<sub>1</sub> 读入某个数据,T<sub>2</sub> 对该数据做了修改,如果 T<sub>1</sub> 再读这个数据,该数据已经改变,和最开始读入的是不一样的。
|
||||
|
||||
# 隔离级别
|
||||
|
||||
数据库管理系统需要防止数据出现不一致,并且有多种级别可以实现,这些级别称为隔离级别。
|
||||
|
||||
## 未提交读(READ UNCOMMITTED)
|
||||
|
||||
一个事务可以读取自己的未提交数据,也被称为脏读。
|
||||
|
||||
## 提交读(READ COMMITTED)
|
||||
|
||||
一个事务可以读取自己的已提交数据,但是该数据可能过后就会被其它事务改变,因此也称为不可重复读。
|
||||
|
||||
## 可重复读(REPEATABLE READ)
|
||||
|
||||
保证在同一个事务中多次读取同样的记录结果是一致的。但是会出现幻读的问题,所谓幻读,指的是某个事务在读取某个范围内的记录时,其它事务会在范围内插入数据,产生幻行。
|
||||
|
||||
## 可串行化(SERIALIXABLE)
|
||||
|
||||
强制事务串行执行,避免幻行的出现。
|
||||
|
||||
# 可串行化调度
|
||||
|
||||
如果并行的事务的执行结果和某一个串行的方式执行的结果一样,那么可以认为结果是正确的。
|
||||
|
||||
# 封锁类型
|
||||
|
||||
排它锁 (X 锁 ),共享锁 (S 锁 )
|
||||
|
||||
一个事务 T 对数据对象 A 加了 X 锁,T 就可以对 A 进行读取和更新。加锁期间其它事务不能对数据对象 A 加任何其它锁;
|
||||
|
||||
一个事务 T 对数据对象加了 S 锁,T 可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对数据对象 A 加 S 锁,但是不能加 X 锁。
|
||||
|
||||
# 封锁粒度
|
||||
|
||||
粒度可以是整个数据库,也可以是表,行,或者分量。
|
||||
|
||||
粒度越小,开销越大。
|
||||
|
||||
# 封锁协议
|
||||
|
||||
## 三级封锁协议
|
||||
|
||||
(1) 1 级封锁协议
|
||||
|
||||
事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。
|
||||
|
||||
可以解决丢失修改问题;
|
||||
|
||||
(2) 2 级封锁协议
|
||||
|
||||
在 1 级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
|
||||
|
||||
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
|
||||
|
||||
(3) 3 级封锁协议
|
||||
|
||||
在 2 级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
|
||||
|
||||
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
|
||||
|
||||

|
||||
|
||||
## 两段锁协议
|
||||
|
||||
加锁和解锁分为两个阶段进行。两段锁是并行事务可串行化的充分条件,但不是必要条件。
|
||||
|
||||
```html
|
||||
lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B)
|
||||
```
|
||||
|
||||
# 乐观锁和悲观锁
|
||||
|
||||
## 悲观锁
|
||||
|
||||
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
|
||||
|
||||
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被阻塞。
|
||||
|
||||
## 乐观锁
|
||||
|
||||
假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
|
||||
|
||||
Java JUC 中的 atomic 包就是乐观锁的一种实现,AtomicInteger 通过 CAS(Compare And Set)操作实现线程安全的自增。
|
||||
|
||||
乐观锁有两种实现方式,数据版本和时间戳。它们都需要在数据库表中增加一个字段,使用这个字段来判断数据是否过期。例如,数据版本实现方式中,需要在数据库表中增加一个数字类型的 version 字段,当读取数据时,将 version 字段的值一同读出。随后数据每更新一次,对此 version 值加 1。当提交更新的时候,判断读出的 version 和数据库表中的 version 是否一致,如果一致,则予以更新;否则认为是过期数据。
|
||||
|
||||
## MySQL 隐式和显示锁定
|
||||
|
||||
MySQL InnoDB 采用的是两阶段锁协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB 会根据事务隔离级别在需要的时候自动加锁。
|
||||
|
||||
另外,InnoDB 也支持通过特定的语句进行显示锁定,这些语句不属于 SQL 规范:
|
||||
|
||||
- ELECT ... LOCK IN SHARE MODE
|
||||
- SELECT ... FOR UPDATE
|
||||
|
||||
# 范式
|
||||
|
||||
记 A->B 表示 A 函数决定于 B,也就是 B 函数依赖于 A。
|
||||
|
||||
如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
|
||||
|
||||
对于函数依赖 W->A,如果能找到 W 的真子集使得 A 依赖于这个真子集,那么就是部分依赖,否则就是完全依赖;
|
||||
|
||||
以下关系中,Sno 表示学号,Cname 表示课程名,Sname 表示学生姓名,Sdept 表示学院,Mname 表示院长姓名。函数依赖为 (Sno, Cname) -> (Sname, Sdept, Mname)。注:实际开发过程中,不会出现这种表,而是每个实体都放在单独一张表中,然后实体之间的联系表用实体 id 来表示。
|
||||
|
||||

|
||||
|
||||
不符合范式的关系,会产生很多异常。主要有以下四种异常:
|
||||
|
||||
1. 冗余数据
|
||||
2. 修改异常
|
||||
3. 删除异常
|
||||
4. 插入异常,比如如果新插入一个学生的信息,而这个学生还没选课,那么就无法插入该学生。
|
||||
|
||||
关系数据库的范式理论就是是为了解决这四种异常。
|
||||
|
||||
高级别范式的依赖基于低级别的范式。
|
||||
|
||||
## 第一范式 (1NF)
|
||||
|
||||
属性不可分。
|
||||
|
||||
## 第二范式 (2NF)
|
||||
|
||||
每个非主属性完全函数依赖于键码。
|
||||
|
||||
可以通过分解来满足。
|
||||
|
||||
**分解前**
|
||||
|
||||
S(Sno, Cname, Sname, Sdept, Mname)
|
||||
|
||||
(Sno, Cname) -> (Sname, Sdept, Mname)
|
||||
|
||||
**分解后**
|
||||
|
||||
S1(Sno, Sname, Sdept, Mname)
|
||||
|
||||
(Sno) -> (Sname, Sdept, Mname)
|
||||
|
||||
(Sdept) -> (Mname)
|
||||
|
||||

|
||||
|
||||
S2(Sno, Cname, Grade)
|
||||
|
||||
(Sno, Cname) -> (Grade)
|
||||
|
||||

|
||||
|
||||
|
||||
## 第三范式 (3NF)
|
||||
|
||||
非主属性不传递依赖于键码。
|
||||
|
||||
上述 S1 存在传递依赖,Mname 依赖于 Sdept,而 Sdept 又依赖于 Sno,可以继续分解。
|
||||
|
||||

|
||||
|
||||
## BC 范式(BCNF)
|
||||
|
||||
所有属性不传递依赖于键码。
|
||||
|
||||
关系模式 STC(Sname, Tname, Cname, Grade),其中四个属性分别为学生姓名、教师姓名、课程名和成绩。有以下函数依赖:
|
||||
|
||||
(Sname, Cname) -> (Tname)
|
||||
|
||||
(Sname, Cname) -> (Grade)
|
||||
|
||||
(Sname, Tname) -> (Cname)
|
||||
|
||||
(Sname, Tname) -> (Grade)
|
||||
|
||||
(Tname) -> (Cname)
|
||||
|
||||
分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST,属性之间是多对多关系,无函数依赖。
|
||||
|
||||
# 约束
|
||||
|
||||
## 键码
|
||||
|
||||
用于唯一表示一个实体。键码可以由多个属性构成,每个构成键码的属性成为码。
|
||||
|
||||
## 单值约束
|
||||
|
||||
某个属性的值是唯一的。
|
||||
|
||||
## 引用完整性约束
|
||||
|
||||
一个实体的属性引用的值在另一个实体的某个属性中存在。
|
||||
|
||||
## 域约束
|
||||
|
||||
某个属性的值在特定范围之内。
|
||||
|
||||
## 一般约束
|
||||
|
||||
一般性约束,比如大小约束,数量约束。
|
||||
|
||||
# 数据库的三层模式和两层映像
|
||||
|
||||
外模式:局部逻辑结构;模式:全局逻辑结构;内模式:物理结构。
|
||||
|
||||
## 外模式
|
||||
|
||||
又称用户模式,是用户和数据库系统的接口,特定的用户只能访问数据库系统提供给他的外模式中的数据。例如不同的用户创建了不同数据库,那么一个用户只能访问他有权限访问的数据库。一个数据库可以有多个外模式,一个用户只能有一个外模式,但是一个外模式可以给多个用户使用。
|
||||
|
||||
## 模式
|
||||
|
||||
可以分为概念模式和逻辑模式,概念模式可以用概念 - 关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系,数据的完整性与安全性要求。
|
||||
|
||||
## 内模式
|
||||
|
||||
又称为存储模式,描述记录的存储方式,例如索引的组织方式、数据是否压缩以及是否加密等等。
|
||||
|
||||
## 外模式/模式映像
|
||||
|
||||
把外模式的局部逻辑结构和模式的全局逻辑结构联系起来。该映像可以保证数据和应用程序的逻辑独立性。
|
||||
|
||||
## 模式/内模式映像
|
||||
|
||||
把模式的全局逻辑结构和内模式的物理结构联系起来,该映像可以保证数据和应用程序的物理独立性。
|
||||
|
||||
# ER 图
|
||||
|
||||
Entity-Relationship,包含三个部分:实体、属性、联系。
|
||||
|
||||
## 实体的三种联系
|
||||
|
||||
联系包含 1 对 1,1 对多,多对多三种。
|
||||
|
||||
如果 A 到 B 是 1 对多关系,那么画个带箭头的线段指向 B;如果是 1 对 1,画两个带箭头的线段;如果是多对多,画两个不带箭头的线段。
|
||||
|
||||

|
||||
|
||||
## 表示出现多次的关系
|
||||
|
||||
一个实体在联系出现几次,就要用几条线连接。如下表示一个课程的先修关系,先修关系中,应当出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
|
||||
|
||||

|
||||
|
||||
## 联系的多向性
|
||||
|
||||
下图中一个联系表示三个实体的关系。虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
|
||||
|
||||

|
||||
|
||||
一般只使用二元联系,可以把多元关系转换为二元关系。
|
||||
|
||||

|
||||
|
||||
## 表示子类
|
||||
|
||||
用 is-a 联系来表示子类,具体做法是用一个三角形和两条线来连接类和子类。与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
|
||||
|
||||

|
||||
|
||||
# 一些概念
|
||||
|
||||
**数据模型** 由数据结构、数据操作和完整性三个要素组成。
|
||||
|
||||
**数据库系统** 包括了数据库,数据库管理系统,应用程序以及数据库管理员和用户,还包括相关的硬件和软件。也就是说数据库系统包含所有与数据库相关的内容。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
|
||||
- [MySQL 乐观锁与悲观锁 ](https://www.jianshu.com/p/f5ff017db62a)
|
@ -1,115 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 地址
|
||||
|
||||
[Google Java编程风格指南](http://www.hawstein.com/posts/google-java-style.html)
|
||||
|
||||
# 命名约定
|
||||
|
||||
在 Google 其它编程语言风格中使用的特殊前缀或后缀,如 name_ , mName , s_name 和 kName ,在 Java 编程风格中都不再使用。
|
||||
|
||||
**1. 包名**
|
||||
|
||||
包名全部小写,连续的单词只是简单地连接起来,不使用下划线。
|
||||
|
||||
**2. 类名**
|
||||
|
||||
类名都以 UpperCamelCase 风格编写。
|
||||
|
||||
类名通常是名词或名词短语,接口名称有时可能是形容词或形容词短语。
|
||||
|
||||
测试类的命名以它要测试的类的名称开始,以 Test 结束。
|
||||
|
||||
**3. 方法名**
|
||||
|
||||
方法名都以 lowerCamelCase 风格编写。
|
||||
|
||||
方法名通常是动词或动词短语。
|
||||
|
||||
下划线可能出现在JUnit测试方法名称中用以分隔名称的逻辑组件。一个典型的模式是: test<MethodUnderTest>\_<state> ,例如 testPop_emptyStack 。 并不存在唯一正确的方式来命名测试方法。
|
||||
|
||||
**4. 常量名**
|
||||
|
||||
常量名命名模式为 CONSTANT_CASE ,全部字母大写,用下划线分隔单词。
|
||||
|
||||
每个常量都是一个静态final字段,但不是所有静态final字段都是常量。
|
||||
|
||||
**5. 非常量字段名**
|
||||
|
||||
非常量字段名以 lowerCamelCase 风格编写。
|
||||
|
||||
**6. 参数名**
|
||||
|
||||
参数名以 lowerCamelCase 风格编写。
|
||||
|
||||
**7. 局部变量名**
|
||||
|
||||
局部变量名以 lowerCamelCase 风格编写
|
||||
|
||||
**8. 类型变量名**
|
||||
|
||||
类型变量可用以下两种风格之一进行命名:
|
||||
|
||||
- 单个的大写字母,后面可以跟一个数字(如:E, T, X, T2)。
|
||||
- 以类命名方式,后面加个大写的 T (如:RequestT, FooBarT)。
|
||||
|
||||
**9. 驼峰式命名法**
|
||||
|
||||
1. 把短语转换为纯 ASCII 码,并且移除任何单引号。例如:"Müller’s algorithm" 将变成 "Muellers algorithm"。
|
||||
2. 把这个结果切分成单词,在空格或其它标点符号(通常是连字符)处分割开。
|
||||
3. 现在将所有字母都小写(包括缩写),然后将单词的第一个字母大写。
|
||||
4. 最后将所有的单词连接起来得到一个标识符。
|
||||
|
||||
```html
|
||||
Prose form Correct Incorrect
|
||||
------------------------------------------------------------------
|
||||
"XML HTTP request" XmlHttpRequest XMLHTTPRequest
|
||||
"new customer ID" newCustomerId newCustomerID
|
||||
"inner stopwatch" innerStopwatch innerStopWatch
|
||||
"supports IPv6 on iOS?" supportsIpv6OnIos supportsIPv6OnIOS
|
||||
"YouTube importer" YouTubeImporter
|
||||
YoutubeImporter*
|
||||
```
|
||||
|
||||
# 编程实践
|
||||
|
||||
- 覆盖的方法加上 @Override 注释。
|
||||
|
||||
- 异常不能忽视,否则需要在 catch 中注释说明。
|
||||
|
||||
```
|
||||
try {
|
||||
int i = Integer.parseInt(response);
|
||||
return handleNumericResponse(i);
|
||||
} catch (NumberFormatException ok) {
|
||||
// it's not numeric; that's fine, just continue
|
||||
}
|
||||
return handleTextResponse(response);
|
||||
```
|
||||
|
||||
- 使用类调用静态成员
|
||||
|
||||
```java
|
||||
Foo.aStaticMethod();
|
||||
```
|
||||
|
||||
- 禁止使用 finalize()
|
||||
|
||||
# Javadoc
|
||||
|
||||
基本格式如下:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Multiple lines of Javadoc text are written here,
|
||||
* wrapped normally...
|
||||
*/
|
||||
```
|
||||
|
||||
或者单行形式:
|
||||
```
|
||||
/** An especially short bit of Javadoc. */
|
||||
```
|
||||
|
||||
标准的 Javadoc 标记按以下顺序出现:@param, @return, @throws, @deprecated,前面这4种标记如果出现,描述都不能为空。当描述无法在一行中容纳,连续行需要至少再缩进4个空格。
|
||||
|
@ -1,193 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 智力题
|
||||
|
||||
**1. 有 20 瓶药丸,其中 19 瓶装有 1 克/粒的药丸,余下一瓶装有 1.1 克/粒的药丸。给你一台称重精准的天平,怎么找出比较重的那瓶药丸?天平只能用一次。**
|
||||
|
||||
可以从药丸的数量上来制造差异:从第 i 瓶药丸中取出 i 个药丸,然后一起称重。可以知道,如果第 i 瓶药丸重 1.1 克/粒,那么称重结果就会比正常情况下重 0.1 * i 克。
|
||||
|
||||
**2. 有个 8×8 棋盘,其中对角的角落上,两个方格被切掉了。给定 31 块多米诺骨牌,一块骨牌恰好可以覆盖两个方格。用这 31 块骨牌能否盖住整个棋盘?**
|
||||
|
||||

|
||||
|
||||
将棋盘划分成黑白两种颜色,每种颜色不相邻,那么一块骨牌需要同时覆盖黑色和白色两个方格。假设切掉的两个对角的方格是黑色的,那么只剩下 32 块白色方格和 30 块黑色方格,因此不能用 31 块骨牌覆盖。
|
||||
|
||||
**3. 有两个水壶,容量分别为 5 夸脱和 3 夸脱,若水的供应不限量,怎么用这两个水壶得到刚好 4 夸脱的水?**
|
||||
|
||||

|
||||
|
||||
**4. 有个岛上住着一群人,有一天来了个游客,定了一条奇怪的规矩:所有蓝眼睛的人都必须尽快离开这个岛。 每晚 8 点会有一个航班离岛。每个人都看得见别人眼睛的颜色,但不知道自己的(别人也不可以告知)。此外,他们不知道岛上到底有多少人是蓝眼睛的,只知道至少有一个人的眼睛是蓝色的。所有蓝眼睛的人要花几天才能离开这个岛?**
|
||||
|
||||
假定这个岛上一共有 n 人,其中 c 人有蓝眼睛。
|
||||
|
||||
1\. 情况 c = 1
|
||||
假设岛上所有人都是聪明的,蓝眼睛的人四处观察之后,发现没有人是蓝眼睛的。但他知道至少有一人是蓝眼睛的,于是就能推导出自己一定是蓝眼睛的。因此,他会搭乘当晚的飞机离开。
|
||||
|
||||
2\. 情况 c = 2
|
||||
两个蓝眼睛的人看到对方,并不确定 c 是 1 还是 2,但是由上一种情况,他们知道,如果 c = 1,那个蓝眼睛的人第一晚就会离岛。因此,发现另一个蓝眼睛的人仍在岛上,他一定能推断出 c = 2,也就意味着他自己也是蓝眼睛的。于是,两个蓝眼睛的人都会在第二晚离岛。
|
||||
|
||||
3\. 情况 c > 2:一般情况
|
||||
逐步提高 c 时,我们可以看出上述逻辑仍旧适用。如果 c = 3,那么,这三个人会立即意识到有 2 到 3 人是蓝眼睛的。如果有两人是蓝眼睛的,那么这两人会在第二晚离岛。因此,如果过了第二晚另外两人还在岛上,每个蓝眼睛的人都能推断出 c = 3,因此这三人都有蓝眼睛。他们会在第三晚离岛。不论 c 为什么值,都可以套用这个模式。所以,如果有 c 人是蓝眼睛的,则 **所有蓝眼睛的人要用 c 晚才能离岛,且都在同一晚离开**。
|
||||
|
||||
**5. 有栋建筑物高 100 层。若从第 N 层或更高的楼层扔下来,鸡蛋就会破掉。若从第 N 层以下的楼层扔下来则不会破掉。给你 2 个鸡蛋,请找出 N,并要求最差情况下扔鸡蛋的次数为最少。**
|
||||
|
||||
首先,让我们试着从 10 层开始扔鸡蛋,然后是 20 层,等等。
|
||||
1\. 如果鸡蛋1第一次扔下楼(10层)就破掉了,那么,最多需要扔 10 次。
|
||||
2\. 如果鸡蛋1最后一次扔下楼(100层)才破掉,那么,最多要扔 19 次(10、 20、…、 90、100 层,然后是 91 到 99 层)。
|
||||
|
||||
这么做也挺不错,但我们只考虑了绝对最差情况。我们应该进行“负载均衡”,让这两种情况下扔鸡蛋的次数更均匀。
|
||||
|
||||
我们的目标是设计一种扔鸡蛋的方法,使得扔鸡蛋1时,不论是在第一次还是最后一次扔下楼才破掉,次数越稳定越好。
|
||||
|
||||
完美负载均衡要求每次鸡蛋1多扔一次,鸡蛋2就应该少扔一次,这样每种情况下扔鸡蛋的次数都相同。由此可知,鸡蛋1必须从 X 层开始往下扔,然后再往上增加 X - 1 层……直至到达 100 层。
|
||||
|
||||
求解方程式 X + (X-1) + (X-2) + … + 1 = 100,得到X (X + 1) / 2 = 100 → X = 14。
|
||||
|
||||
我们先从 14 层开始,然后是 27 层,接着是 39 层,依此类推,最差情况下鸡蛋要扔 14 次。
|
||||
|
||||
正如解决其他许多最大化/最小化的问题一样,这类问题的关键在于“平衡最差情况”。
|
||||
|
||||
**6. 走廊上有 100 个关上的储物柜。有个人先是将 100 个柜子全都打开。接着,每数两个柜子关上一个。然后,在第三轮时,再每隔两个就切换第三个柜子的开关状态。照此规律反复操作 100 次,在第 i 轮,这个人会每数 i 个就切换第 i 个柜子的状态。当第 100 轮经过走廊时,只切换第 100 个柜子的开关状态,此时有几个柜子是开着的?**
|
||||
|
||||
1\. 问题:柜子会在哪几轮切换状态?
|
||||
柜子n会在n的每个因子(包括 1 和 n 本身)对应的那一轮切换状态。也就是说,柜子15会在第 1、 3、 5 和 15 轮开或关一次。
|
||||
|
||||
2\. 问题:柜子什么时候还是开着的?
|
||||
如果因子个数(记作 x )为奇数,则这个柜子是开着的。你可以把一对因子比作开和关,若还剩一个因子,则柜子就是开着的。
|
||||
|
||||
3\. 问题: x 什么时候为奇数?
|
||||
若 n 为完全平方数,则 x 的值为奇数。理由如下:将 n 的两个互补因子配对。例如,如 n 为 36,则因子配对情况为: (1, 36)、 (2, 18)、 (3, 12)、 (4, 9)、 (6, 6)。注意, (6, 6) 其实只有一个因子,因此 n 的因子个数为奇数。
|
||||
4\. 问题:有多少个完全平方数?
|
||||
一共有 10 个完全平方数:1\*1, 2\*2, 3\*3, ..., 10\*10
|
||||
|
||||
因此,**最后共有 10 个柜子是开着的**。
|
||||
|
||||
**7. 给定两条绳子,每条绳子燃烧殆尽正好要用一个小时。怎样用这两条绳子准确计量 15 分钟?注意这些绳子密度不均匀,因此烧掉半截绳子不一定正好用半个小时。**
|
||||
|
||||
1\. 点燃绳子 1 两头的同时,点燃绳子 2 的一头。
|
||||
2\. 当绳子 1 烧完,正好过去 30 分钟。而绳子 2 还可以再烧 30 分钟。
|
||||
3\. 点燃绳子 2 的另一头。
|
||||
4\. 15 分钟后,绳子 2 将全部烧完。
|
||||
|
||||
**8. 给定 9 个球,其中 8 个球的重量相同,只有一个比较重。然后给定一个天平,可以称出左右两边哪边更重。最多用两次天平,找出这个重球。**
|
||||
|
||||
将这些球均分成 3 个一组共 3 组,称量一次就能知道哪一组球更重。我们甚至可以总结出一条规律:给定 N 个球,其中 N 能被 3 整除,称量一次便能找到包含重球的那一组球。
|
||||
|
||||
找到这一组 3 个球之后,我们只是简单地重复此前的模式:先把一个球放到一边,称量剩下的两个球。从中挑出那个重球;或者,如果这两个球重量相同,那第 3 个球便是重球。
|
||||
|
||||
**9. 考虑一个双人游戏。游戏在一个圆桌上进行。每个游戏者都有足够多的硬币。他们需要在桌子上轮流放置硬币,每次必需且只能放置一枚硬币,要求硬币完全置于桌面内(不能有一部分悬在桌子外面),并且不能与原来放过的硬币重叠。谁没有地方放置新的硬币,谁就输了。游戏的先行者还是后行者有必胜策略?**
|
||||
|
||||
先行者在桌子中心放置一枚硬币,以后的硬币总是放在与后行者刚才放的地方相对称的位置。这样,只要后行者能放,先行者一定也有地方放。先行者必胜。
|
||||
|
||||
**10. 一个矩形蛋糕,蛋糕内部有一块矩形的空洞。只用一刀,如何将蛋糕切成大小相等的两块?**
|
||||
|
||||
注意到平分矩形面积的线都经过矩形的中心。过大矩形和空心矩形各自的中心画一条线,这条线显然把两个矩形都分成了一半,它们的差当然也是相等的。
|
||||
|
||||
**11. 一块矩形的巧克力,初始时由 N x M 个小块组成。每一次你只能把一块巧克力掰成两个小矩形。最少需要几次才能把它们掰成 N x M 块 1x1 的小巧克力?**
|
||||
|
||||
因为每掰一次后当前巧克力的块数只能增加 1,把巧克力分成 N x M 块当然需要至少掰 N x M - 1 次。
|
||||
|
||||
**12. 地球上有多少个点,使得从该点出发向南走一英里,向东走一英里,再向北走一英里之后恰好回到了起点?**
|
||||
|
||||
“北极点”是一个传统的答案,其实这个问题还有其它的答案。事实上,满足要求的点有无穷多个。所有距离南极点 1 + 1/(2π) 英里的地方都是满足要求的,向南走一英里后到达距离南极点 1/(2π) 的地方,向东走一英里后正好绕行纬度圈一周,再向北走原路返回到起点。事实上,这仍然不是满足要求的全部点。距离南极点 1 + 1/(2kπ) 的地方都是可以的,其中 k 可以是任意一个正整数。
|
||||
|
||||
**13. A、B 两人分别在两座岛上。B 生病了,A 有 B 所需要的药。C 有一艘小船和一个可以上锁的箱子。C 愿意在 A 和 B 之间运东西,但东西只能放在箱子里。只要箱子没被上锁,C 都会偷走箱子里的东西,不管箱子里有什么。如果 A 和 B 各自有一把锁和只能开自己那把锁的钥匙,A 应该如何把东西安全递交给 B ?**
|
||||
|
||||
A 把药放进箱子,用自己的锁把箱子锁上。B 拿到箱子后,再在箱子上加一把自己的锁。箱子运回 A 后,A 取下自己的锁。箱子再运到 B 手中时,B 取下自己的锁,获得药物。
|
||||
|
||||
**14. 一对夫妇邀请 N-1 对夫妇参加聚会(因此聚会上总共有 2N 人)。每个人都和所有自己不认识的人握了一次手。然后,男主人问其余所有人(共 2N-1 个人)各自都握了几次手,得到的答案全部都不一样。假设每个人都认识自己的配偶,那么女主人握了几次手?**
|
||||
|
||||
握手次数只可能是从 0 到 2N-2 这 2N-1 个数。除去男主人外,一共有 2N-1 个人,因此每个数恰好出现了一次。其中有一个人(0)没有握手,有一个人(2N-2)和所有其它的夫妇都握了手。这两个人肯定是一对夫妻,否则后者将和前者握手(从而前者的握手次数不再是 0)。除去这对夫妻外,有一个人(1)只与(2N-2)握过手,有一个人(2N-3)和除了(0)以外的其它夫妇都握了手。这两个人肯定是一对夫妻,否则后者将和前者握手(从而前者的握手次数不再是 1)。以此类推,直到握过 N-2 次手的人和握过 N 次手的人配成一对。此时,除了男主人及其配偶以外,其余所有人都已经配对。根据排除法,最后剩下来的那个握手次数为 N-1 的人就是女主人了。
|
||||
|
||||
**15. 你在一幢 100 层大楼下,有 21 根电线线头标有数字 1..21。这些电线一直延伸到大楼楼顶,楼顶的线头处标有字母 A..U。你不知道下面的数字和上面的字母的对应关系。你有一个电池,一个灯泡,和许多很短的电线。如何只上下楼一次就能确定电线线头的对应关系?**
|
||||
|
||||
在下面把 2,3 连在一起,把 4 到 6 全连在一起,把 7 到 10 全连在一起,等等,这样你就把电线分成了 6 个“等价类”,大小分别为 1, 2, 3, 4, 5, 6。然后到楼顶,测出哪根线和其它所有电线都不相连,哪些线和另外一根相连,哪些线和另外两根相连,等等,从而确定出字母 A..U 各属于哪个等价类。现在,把每个等价类中的第一个字母连在一起,形成一个大小为 6 的新等价类;再把后 5 个等价类中的第二个字母连在一起,形成一个大小为 5 的新等价类;以此类推。回到楼下,把新的等价类区别出来。这样,你就知道了每个数字对应了哪一个原等价类的第几个字母,从而解决问题。
|
||||
|
||||
**16. 某种药方要求非常严格,你每天需要同时服用 A、B 两种药片各一颗,不能多也不能少。这种药非常贵,你不希望有任何一点的浪费。一天,你打开装药片 A 的药瓶,倒出一粒药片放在手心;然后打开另一个药瓶,但不小心倒出了两粒药片。现在,你手心上有一颗药片 A,两颗药片 B,并且你无法区别哪个是 A,哪个是 B。你如何才能严格遵循药方服用药片,并且不能有任何的浪费?**
|
||||
|
||||
把手上的三片药各自切成两半,分成两堆摆放。再取出一粒药片 A,也把它切成两半,然后在每一堆里加上半片的 A。现在,每一堆药片恰好包含两个半片的 A 和两个半片的 B。一天服用其中一堆即可。
|
||||
|
||||
**17. 你在一个飞船上,飞船上的计算机有 n 个处理器。突然,飞船受到外星激光武器的攻击,一些处理器被损坏了。你知道有超过一半的处理器仍然是好的。你可以向一个处理器询问另一个处理器是好的还是坏的。一个好的处理器总是说真话,一个坏的处理器总是说假话。用 n-2 次询问找出一个好的处理器。**
|
||||
|
||||
给处理器从 1 到 n 标号,并用符号 a->b 表示向标号为 a 的处理器询问处理器 b 是不是好的。
|
||||
|
||||
首先 1->2,如果 1 说不是,就把他们俩都去掉(去掉了一个好的和一个坏的,则剩下的处理器中好的仍然过半),然后从 3->4 开始继续发问;如果 1 说 2 是好的,就继续问 2->3,3->4,…… 直到某一次 j 说 j+1 是坏的,把 j 和 j+1 去掉,然后问 j-1 -> j+2,如果前面已经没有 j-1 了,从 j+2 -> j+3 开始发问。
|
||||
|
||||
注意到你始终维护着这样一个“链”,前面的每一个处理器都说后面那个是好的。这条链里的所有处理器要么都是好的,要么都是坏的。当这条链越来越长,剩下的处理器越来越少时,总有一个时候这条链超过了剩下的处理器的一半,此时可以肯定这条链里的所有处理器都是好的。或者,越来越多的处理器都被去掉了,链的长度依旧为 0,而最后只剩下一个或两个处理器没被问过,那他们一定就是好的了。另外注意到,第一个处理器的好坏从来没被问过,仔细想想你会发现最后一个处理器的好坏也不可能被问到(一旦链长超过剩余处理器的一半,或者最后没被去掉的就只剩这一个了时,你就不问了),因此询问次数不会超过 n-2。
|
||||
|
||||
**18. 有 25 匹马,速度都不同,但每匹马的速度都是定值。现在只有 5 条赛道,无法计时,即每赛一场最多只能知道 5 匹马的相对快慢。问最少赛几场可以找出 25 匹马中速度最快的前 3 名?(百度2008年面试题)**
|
||||
|
||||
每匹马都至少要有一次参赛的机会,所以 25 匹马分成 5 组,一开始的这 5 场比赛是免不了的。接下来要找冠军也很容易,每一组的冠军在一起赛一场就行了(第 6 场)。最后就是要找第 2 和第 3 名。我们按照第 6 场比赛中得到的名次依次把它们在前 5 场比赛中所在的组命名为 A、B、C、D、E。即:A 组的冠军是第 6 场的第 1 名,B 组的冠军是第 6 场的第 2 名……每一组的 5 匹马按照他们已经赛出的成绩从快到慢编号:
|
||||
|
||||
A组:1,**2,3**,4,5
|
||||
B组:**1,2**,3,4,5
|
||||
C组:**1,**2,3,4,5
|
||||
D组:1,2,3,4,5
|
||||
E组:1,2,3,4,5
|
||||
|
||||
从现在所得到的信息,我们可以知道哪些马已经被排除在 3 名以外。只要已经能确定有 3 匹或 3 匹以上的马比这匹马快,那么它就已经被淘汰了。可以看到,只有上表中粗体的那 5 匹马才有可能为 2、3 名的。即:A 组的 2、3 名;B 组的 1、2 名,C 组的第 1 名。取这 5 匹马进行第 7 场比赛,第 7 场比赛的前两名就是 25 匹马中的 2、3 名。故一共最少要赛 7 场。
|
||||
|
||||
**19. 有 7 克、2 克砝码各一个,天平一只,如何只用这些物品三次将140 克的盐分成 50、90 克各一份?**
|
||||
|
||||
第一步:把 140 克盐分成两等份,每份 70 克。
|
||||
第二步:把天平一边放上 2+7 克砝码,另一边放盐,这样就得到 9 克和 61 克分开的盐。
|
||||
第三步:将 9 克盐和 2 克砝码放在天平一边,另一边放盐,这样就得到 11 克和 50 克。于是 50 和 90 就分开了。
|
||||
|
||||
**20. 有三筐水果,一筐装的全是苹果,第二筐装的全是橘子,第三筐是橘子与苹果混在一起。筐上的标签都是骗人的,(比如,如果标签写的是橘子,那么可以肯定筐里不会只有橘子,可能还有苹果)你的任务是拿出其中一筐,从里面只拿一只水果,然后正确写出三筐水果的标签。**
|
||||
|
||||
从贴有苹果和橘子标签的筐中拿出一个水果,如果是苹果,说明这个筐中全是苹果,那么贴苹果标签的筐里装的全是桔子,则贴有桔子标签的筐中装的苹果和桔子;如果拿出的一个水果是桔子,说明这个筐中全是桔子,那么贴桔子标签的筐里装的全是苹果,贴苹果标签的筐里装的是苹果和桔子。
|
||||
|
||||
# 数字推理
|
||||
|
||||
数列常见的有等差等比关系,等差关系中数之间的 **间隔** 相同,等比关系中数之间的 **比值** 相同。不规则的数列,间隔和比值都不会相同,但是间隔和比值又可以当做一个数列,并且这个数列具有等差等比关系。例如 3,4,6,9,13,18,间隔数列为 1, 2, 3, 4, 5 为等差数列,8,8,12,24,60,180,比值数列为 1, 1.5, 2, 2.5 为等比数列。
|
||||
|
||||
数列也可能和 **平方、立方、次方** 相关。例如 66,83,102,123,146 为 8, 9, 10, 11, 12 的平方再加 2 所得,0,6,24,60,120,210 为 n<sup>3</sup> - 1。
|
||||
|
||||
当相邻的数都没规律时,考虑 **错位** 的情况。例如 5,4,10,8,15,16,20,32 ,奇数列和偶数列分别为一个等差数列和一个等比数列。
|
||||
|
||||
数列中一个数不一定只和前一个数有关,有可能 **和前两个数有关**,例如 2,5,10,50,500,第三个数为前两个数的乘积。
|
||||
|
||||
# 图形推理
|
||||
|
||||
**1. 结构类**
|
||||
|
||||
- 对称性(水平对称、垂直对称、中心对称)
|
||||
- 直曲性(直线、曲线)
|
||||
- 封闭与开放
|
||||
|
||||
**2. 位置类**
|
||||
|
||||
- 相对位置(相交、相离、相切;相对与相邻;独立于覆盖)
|
||||
- 移动、旋转、翻转
|
||||
|
||||
**3. 叠加类**
|
||||
|
||||
- 直接叠加
|
||||
- 去同存异
|
||||
- 去异存同
|
||||
- 自定义叠加(阴影变化)
|
||||
|
||||
**4. 组合类**
|
||||
|
||||
**5. 数量类**
|
||||
|
||||
- 点(十字交叉点、T 字交叉点、切点、接触点数量)
|
||||
- 线(线条数:直线数、曲线数、线条总数;笔画数;一笔画与多笔画)
|
||||
- 角(锐角、直角、钝角的数量)
|
||||
- 面(封闭区域数、面积大小、面的个数)
|
||||
|
||||
数量关系有:
|
||||
|
||||
- 相同
|
||||
- 等差
|
||||
- 等比
|
||||
- 和相等
|
||||
|
||||

|
||||
|
||||
# 参考资料
|
||||
|
||||
1. 程序员面试金典
|
||||
2. [数字推理题的各种规律范例题型](http://www.linquan.info/archives/62.html)
|
||||
3. [程序员有趣的面试智力题](http://blog.csdn.net/hackbuteer1/article/details/6726419)
|
||||
4. [图形推理50题 答案及解析](http://blog.sina.com.cn/s/blog_7815fa370100tdgm.html)
|
@ -1,391 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第 1 章 概述
|
||||
|
||||
正则表达式用于文本内容的查找和替换。
|
||||
|
||||
正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。
|
||||
|
||||
一个问题往往可以用多种正则表达式方案来解决。
|
||||
|
||||
[ 正则表达式在线工具 ](http://tool.chinaz.com/regex)
|
||||
|
||||
# 第 2 章 匹配单个字符
|
||||
|
||||
正则表达式一般是区分大小写的,但是也有些实现是不区分。
|
||||
|
||||
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
|
||||
|
||||
**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。
|
||||
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
nam.
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
My **name** is Zheng.
|
||||
|
||||
|
||||
# 第 3 章 匹配一组字符
|
||||
|
||||
**[ ]** 定义一个字符集合;
|
||||
|
||||
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定。字符区间只能用在 [ ] 之间,因此 **-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
|
||||
|
||||
**^** 是取非操作,必须在 [ ] 字符集合中使用;
|
||||
|
||||
**应用**
|
||||
|
||||
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
abc[^0-9]
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**abcd**
|
||||
abc1
|
||||
abc2
|
||||
|
||||
# 第 4 章 使用元字符
|
||||
|
||||
## 4.1 匹配空白字符
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| [\b] | 回退(删除)一个字符 |
|
||||
| \f | 换页符 |
|
||||
| \n | 换行符 |
|
||||
| \r | 回车符 |
|
||||
| \t | 制表符 |
|
||||
| \v | 垂直制表符 |
|
||||
|
||||
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n ;\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
|
||||
|
||||
. 是元字符,前提是没有对它们进行转义; f 和 n 也是元字符,但是前提是对他们进行了转义。
|
||||
|
||||
## 4.2 匹配特定的字符类别
|
||||
|
||||
**1. 数字元字符**
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| \d | 数字字符,等价于 [0-9] |
|
||||
| \D | 非数字字符,等价于 [^0-9] |
|
||||
|
||||
**2. 字母数字元字符**
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
|
||||
| \W | 对 \w 取非 |
|
||||
|
||||
**3. 空白字符元字符**
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| ------------ | ------------ |
|
||||
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
|
||||
| \S | 对 \s 取非 |
|
||||
|
||||
|
||||
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n,也就是它会匹配 \n 。
|
||||
|
||||
## 4.3 使用 POSIX 字符类
|
||||
|
||||
| 字符类 | 说明 |
|
||||
| --- | --- |
|
||||
| [:alnum:] | 字母数字字符 |
|
||||
| [:alpha:] | 字母字符 |
|
||||
| [:cntrl:] | 控制字符 |
|
||||
| [:digit:] | 数字字符 |
|
||||
| [:graph:] | 非空白字符 ( 非空格、控制字符等 ) |
|
||||
| [:lower:] | 小写字母 |
|
||||
| [:print:] | 与 [:graph:] 相似,但是包含空格字符 |
|
||||
| [:punct:] | 标点字符 |
|
||||
| [:space:] | 所有的空白字符 ( 换行符、空格、制表符 ) |
|
||||
| [:upper:] | 大写字母 |
|
||||
| [:xdigit:] | 允许十六进制的数字 (0-9a-fA-F) |
|
||||
|
||||
并不是所有正则表达式实现都支持 POSIX 字符类,也不一定使用它。
|
||||
|
||||
使用时需要用两对方括号,例如 [[:alpha:]]。
|
||||
|
||||
# 第 5 章 重复匹配
|
||||
|
||||
**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配邮箱地址。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
[\w.]+@\w+.\w+
|
||||
```
|
||||
|
||||
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**abc.def@qq.com**
|
||||
|
||||
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义确实相同的。
|
||||
|
||||
```
|
||||
\w+@\w+.\w+
|
||||
[\w]+@[\w]+.[\w]+
|
||||
```
|
||||
|
||||
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m~n 个字符,**{m,}** 至少匹配 m 个字符;
|
||||
|
||||
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
a.+c
|
||||
```
|
||||
|
||||
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容,所以会把整个 abcabcabc 文本都匹配,而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**abcabcabc**
|
||||
|
||||
# 第 6 章 位置匹配
|
||||
|
||||
## 6.1 单词边界
|
||||
|
||||
**\b** 可以匹配一个单词的边界,边界是指位于 \w 和 \W 之间的位置;**\B** 匹配一个不是单词边界的位置。
|
||||
|
||||
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
|
||||
|
||||
## 6.2 字符串边界
|
||||
|
||||
**^** 匹配整个字符串的开头,**$** 匹配结尾。
|
||||
|
||||
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
|
||||
|
||||
使用 (?m) 来打开分行匹配模式,在该模式下,换行被当做字符串的边界。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配代码中以 // 开始的注释行
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
(?m)^\s*//.*$
|
||||
```
|
||||
|
||||
如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容,因为 * 是贪婪型的。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。
|
||||
|
||||
**匹配结果**
|
||||
|
||||
public void fun() {
|
||||
**// 注释 1**
|
||||
int a = 1;
|
||||
int b = 2;
|
||||
**// 注释 2**
|
||||
int c = a + b;
|
||||
}
|
||||
|
||||
# 第 7 章 使用子表达式
|
||||
|
||||
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
|
||||
|
||||
子表达式可以嵌套,但是嵌套层次过深会变得很难理解。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
(ab) {2,}
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**ababab**
|
||||
|
||||
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
|
||||
|
||||
```
|
||||
(19|20)\d{2}
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**1900
|
||||
2010**
|
||||
1020
|
||||
|
||||
**应用**
|
||||
|
||||
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
|
||||
|
||||
① 一位或者两位的数字
|
||||
② 1 开头的三位数
|
||||
③ 2 开头,第 2 位是 0-4 的三位数
|
||||
④ 25 开头,第 3 位是 0-5 的三位数
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
(((\d{1, 2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.) {3}(((\d{1, 2})|(1\d{2})|(2[0-4]\d)|(25[0-5])))
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**192.168.0.1**
|
||||
555.555.555.555
|
||||
|
||||
# 第 8 章 回溯引用
|
||||
|
||||
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc ,那么回溯引用部分也需要匹配 abc 。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配 HTML 中合法的标题元素。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
\1 将回溯引用 子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
|
||||
|
||||
```
|
||||
<(h[1-6])>\w*?</\1>
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**<h1>x</h1>
|
||||
<h2>x</h2>**
|
||||
<h3>x</h1>
|
||||
|
||||
## 替换
|
||||
|
||||
需要用到两个正则表达式。
|
||||
|
||||
**应用**
|
||||
|
||||
修改电话号码格式。
|
||||
|
||||
**文本**
|
||||
|
||||
313-555-1234
|
||||
|
||||
**查找正则表达式**
|
||||
|
||||
```
|
||||
(\d{3})(-)(\d{3})(-)(\d{4})
|
||||
```
|
||||
|
||||
**替换正则表达式**
|
||||
|
||||
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
|
||||
|
||||
```
|
||||
($1) $3-$5
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
(313) 555-1234
|
||||
|
||||
## 大小写转换
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| ---| ---|
|
||||
| \l | 把下个字符转换为小写 |
|
||||
| \u| 把下个字符转换为大写 |
|
||||
| \L | 把\L 和\E 之间的字符全部转换为小写 |
|
||||
| \U | 把\U 和\E 之间的字符全部转换为大写 |
|
||||
| \E | 结束\L 或者\U |
|
||||
|
||||
**应用**
|
||||
|
||||
把文本的第二个和第三个字符转换为大写。
|
||||
|
||||
**文本**
|
||||
|
||||
abcd
|
||||
|
||||
**查找**
|
||||
|
||||
```
|
||||
(\w)(\w{2})(\w)
|
||||
```
|
||||
|
||||
**替换**
|
||||
|
||||
```
|
||||
$1\U$2\E$3
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
aBCd
|
||||
|
||||
# 第 9 章 前后查找
|
||||
|
||||
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义。
|
||||
|
||||
**应用**
|
||||
|
||||
查找出邮件地址 @ 字符前面的部分。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
\w+(?=@)
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
**abc**@qq.com
|
||||
|
||||
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
|
||||
|
||||
# 第 10 章 嵌入条件
|
||||
|
||||
## 10.1 回溯引用条件
|
||||
|
||||
条件判断为某个子表达式是否匹配,如果匹配则需要继续匹配条件表达式后面的内容。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件,当子表达式 1 匹配时条件成立,需要执行 \) 匹配。
|
||||
|
||||
```
|
||||
(\()?abc(?(1)\))
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
**(abc)**
|
||||
**abc**
|
||||
(abc
|
||||
|
||||
## 10.2 前后查找条件
|
||||
|
||||
条件为定义的首尾是否匹配,如果匹配,则继续执行后面的匹配。注意,首尾不包含在匹配的内容中。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4} 。
|
||||
|
||||
```
|
||||
\d{5}(?(?=-)-\d{4})
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
**11111**
|
||||
22222-
|
||||
**33333-4444**
|
@ -1,120 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 译者序
|
||||
|
||||
作者 Bob 大叔,著有《敏捷软件开发:原则、模式与实践》《代码整洁之道》《程序员的职业素养》
|
||||
|
||||
对于解决问题,重要的不是问题本身,而是解决问题的方式、步骤以及反思的深度,这就是职业素养。
|
||||
|
||||
# 前言
|
||||
|
||||
由于管理者面临很大的财务和政治压力,他们无视技术人员发出的危险警告,抱着侥幸心理发射了“挑战者”航天飞机,最终导致航天飞机在高空爆炸。虽说技术人员已经做很多事情去阻止这次发射,但是并不是说做了所有他们能做的,例如他们就没有打电话去新闻台揭露此次发射的危险性。
|
||||
|
||||

|
||||
|
||||
|
||||
# 专业主义
|
||||
|
||||
专业主义不仅意味着荣誉和骄傲,也意味着责任。
|
||||
|
||||
作者在自己开发的一个电话线路故障检测系统时,因为赶着在到期日交付新功能,没有对整个系统进行测试,导致了一个 bug 的出现。作者认为这是一种不负责任的表现,也就是不专业的表现。
|
||||
|
||||
代码中难免有 bug,但是不意味着你不用为 bug 负责。
|
||||
|
||||
有人把 QA 当成查 bug 的机器,等着 QA 发现 bug,而自己却不主动去发现 bug,这是不对的。
|
||||
|
||||
为了保证代码的正确性,需要写一些随时可以运行的单元测试,并且不断地去运行它们。
|
||||
|
||||
软件项目的根本原则 - 易于修改。为了让自己的软件易于修改,需要经常去修改它!
|
||||
|
||||
单元测试可以让程序员更有自信去修改代码,因为它会让人确信修改地是否正确。
|
||||
|
||||
专业素养包括以下内容:
|
||||
|
||||
1. 坚持学习
|
||||
2. 每天练习一些简单的编程题目
|
||||
3. 与他们合作,从彼此身上学到更多东西
|
||||
4. 交流。通过交流表达自己的思想,发现自己的不足以及与别人思想上的差异。
|
||||
5. 了解业务领域,找几本相关领域的书看。
|
||||
6. 与雇主 / 客户保持一致
|
||||
7. 谦逊,因为他们知道自己也可能犯错。
|
||||
|
||||
# 说不
|
||||
|
||||
对一些不合理的要求,不能只是说“试试看”,这会被当成是接受了这个要求。如果自己深知这种不合理的要求会导致严重的后果,就不能任由它发展下去,对这一要求说“不”。
|
||||
|
||||
说“不”时要用最详细的细节来说明,而不是烦躁地反驳。
|
||||
|
||||
当要求明显不合理时,可以协商一些双方都能接受的方案。
|
||||
|
||||
"有可能写出好代码吗" 这篇博文,讲述了一次开发一家零售商为了在"黑色星期五"能够让顾客看到商品信息商店信息以及发布优惠码等功能的 ipone 应用,最开始零售商的经理承诺说只要写个硬编码的应用就行了,但是客户所要的任何一项功能,总比它最开始时所说的要复杂许多。面对时间的紧急,客户方的不配合,以及需求的变更,让博文作者写了一堆很烂的代码,而他自己确实对那些设计模式等高级开发技术是非常热衷的。所以他认为在实际的开发中因为客户的需求等因素,很难写作自己想写的代码。Bob 认为,上面那篇博文的作者才是此次开发过程碰到的那些问题的负责人,因为他没有对那些不合理的要求说不。
|
||||
|
||||
# 说是
|
||||
|
||||
做出承诺的是三个步骤:口头上答应,放在心里,付诸行动。
|
||||
|
||||
缺乏承诺的词语:
|
||||
|
||||
1. 需要:我需要把这事做完;
|
||||
2. 希望:希望今天我能完成任务;
|
||||
3. 让我们:让我们把这件事做完。
|
||||
|
||||
缺乏承诺的人不会把重点放在自己身上,也不会明确说明一件事情的截止日期。真正的承诺是这样的“我会在...之前...”
|
||||
|
||||
对自己无法完全掌握的事,不要轻易承诺。
|
||||
|
||||
发现自己无法做到承诺,立马去调整别人对你的预期。
|
||||
|
||||
“试试看”说明自己对承诺的内容有点顾忌,因此不要用试试看来作为一种承诺。如果不能确信自己能达到承诺的内容而随便承诺,那么最后往往会产生严重的后果。
|
||||
|
||||
# 编程
|
||||
|
||||
要精通一项技术,要对该技术具备“信心”和“感知能力”。
|
||||
|
||||
状态不好的时候最好别写代码。状态不好分为:疲劳,比如熬夜;焦虑,因为外界的事情让自己不能安心下来,这个时候应该下调整好再进行编程。
|
||||
|
||||
进入流状态的感觉很好,觉得可以做很多事,但是流状态其实是一种浅层冥想,思维能力会下降,因此要避免进入流状态。
|
||||
|
||||

|
||||
|
||||
流状态的特点是隔绝沟通,而结对编程能够打破这种隔绝,因此结对编程能够防止进入流状态。
|
||||
|
||||
当自己没法继续编程时,结对编程是个好选择,结对编程时,大脑和身体会有化学变化,也就是说思维方式会改变,有助于突破当前的思维阻塞状态。
|
||||
|
||||
听音乐时音乐会占用一部分脑力资源,因此会导致效率下降,但是这不是绝对。
|
||||
|
||||
多输入一些创造性的内容,比如科幻小说等,自己的创造力也会得到提升。
|
||||
|
||||
调试时间也很宝贵,测试驱动开发能够降低调试的时间。
|
||||
|
||||
编码和跑马拉松一样,无法全程以最快的速度冲刺,只能通过保持体力和维持节奏来取得胜利。因此要保持好自己的节奏,在疲劳的时候适当休息。
|
||||
|
||||
定期做进度衡量,用乐观预估、标准预估和悲观预估这三个时间点。
|
||||
|
||||
如果自己预估的进度赶不上截止时间,不要答应在截止时间去完成,因为你知道根据自己的预估这是不可能的。
|
||||
|
||||
任务完成的标准通常用一个自动化的验收测试来定义。
|
||||
|
||||
帮助他人也会给自己带来好处,在帮助别人的时候不要让自己看起来很仓促,就像是在随便应付。一般帮助别人不需要特别多的时间。也要学会请求别人的帮助,因为自己不可能是万能的,通过别人的帮助能更好的解决问题。
|
||||
|
||||
# 测试驱动开发
|
||||
|
||||
TDD 的运行周期很短,可能一两分钟就可以运行一次程序,短周期可以快速得到反馈,反馈在开发中特别重要,一方面是提高程序员编码的激情,另一方面是能够及早发现 bug,而 bug 越早发现修改地代价也越低。
|
||||
|
||||
TDD 三法则:
|
||||
|
||||
1. 先写测试;
|
||||
2. 在一个单元测试失败的时候,不要再写测试代码;
|
||||
3. 产品代码能恰好通过当前失败的单元测试即可,不要多写。
|
||||
|
||||
TDD 优点
|
||||
|
||||
1. 确定性:确定当前系统是否正确;
|
||||
2. 信心:程序员更有信息去修改混乱的代码,因为通过单元测试可以知道是否修改地正确;
|
||||
3. 文档:单元测试是底层设计细节的文档,通过阅读单元测试能够知道程序的运行方式;
|
||||
|
||||
# 练习
|
||||
|
||||
卡塔在武术里面是一套设计好的招式,该招式模拟真实搏斗场景,习武者不断训练来让身体熟悉该招式,从而做到纯熟。编程卡塔是一套编程过程敲击鼠标和键盘的动作,练习者不是为了解决真正的问题,因为已经知道了解决方案,而是练习解决这个问题所需要的动作和决策。
|
||||
|
||||
瓦萨即使两人对练的卡塔。在编程领域,可以是一个人写单元测试,另一个人写程序通过单元测试。比如,一个人实现排序算法,写测试的人可以很容易地限制速度和内存,给同伴施压。
|
1539
notes/笔记/算法.md.txt
1539
notes/笔记/算法.md.txt
File diff suppressed because it is too large
Load Diff
@ -1,328 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第 1 章 可读性的重要性
|
||||
|
||||
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
|
||||
|
||||
可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。
|
||||
|
||||
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
|
||||
|
||||
# 第 2 章 用名字表达代码含义
|
||||
|
||||
一些比较有表达力的单词:
|
||||
|
||||
| 单词 | 可替代单词 |
|
||||
| --- | --- |
|
||||
| send | deliver、dispatch、announce、distribute、route |
|
||||
| find | search、extract、locate、recover |
|
||||
| start| launch、create、begin、open|
|
||||
|make|create、set up、build、generate、compose、add、new|
|
||||
|
||||
使用 i、j、k 作为循环迭代器不总是好的,为迭代器添加更有表达力的名字会更好,比如 user_i、member_i。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高
|
||||
|
||||
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
|
||||
|
||||
# 第 3 章 名字不能带来歧义
|
||||
|
||||
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
|
||||
|
||||
用 min、max 表示数量范围;
|
||||
|
||||
用 first、last 表示访问空间的包含范围,begin、end 表示访问空间的排除范围,即 end 不包含尾部。
|
||||
|
||||

|
||||
|
||||
布尔相关的命名加上 is、can、should、has 等前缀。
|
||||
|
||||
# 第 4 章 良好的代码风格
|
||||
|
||||
适当的空行和缩进
|
||||
|
||||
排列整齐的注释:
|
||||
|
||||
```
|
||||
int a = 1; // 注释
|
||||
int b = 11; // 注释
|
||||
int c = 111; // 注释
|
||||
```
|
||||
|
||||
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
|
||||
|
||||
把相关的代码按块组织放在一起。
|
||||
|
||||
# 第 5 章 编写注释
|
||||
|
||||
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
|
||||
|
||||
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
|
||||
|
||||
可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。
|
||||
|
||||
注释用来提醒一些特殊情况。
|
||||
|
||||
用 TODO 等做标记:
|
||||
|
||||
| 标记 | 用法 |
|
||||
|---|---|
|
||||
|TODO| 待做 |
|
||||
|FIXME| 待修复 |
|
||||
|HACH| 粗糙的解决方案 |
|
||||
|XXX| 危险!这里有重要的问题 |
|
||||
|
||||
# 第 6 章 如何编写注释
|
||||
|
||||
尽量简洁明了:
|
||||
|
||||
```
|
||||
// The first String is student's name
|
||||
// The Second Integer is student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
```
|
||||
// Student' name -> Student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
添加测试用例来说明:
|
||||
|
||||
```
|
||||
//...
|
||||
// Example: add(1, 2), return 3
|
||||
int add(int x, int y) {
|
||||
return x + y;
|
||||
}
|
||||
```
|
||||
|
||||
在很复杂的函数调用中对每个参数标上名字:
|
||||
|
||||
```
|
||||
int a = 1;
|
||||
int b = 2;
|
||||
int num = add(\* x = *\ a, \* y = *\ b);
|
||||
```
|
||||
|
||||
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
|
||||
|
||||
# 第 7 章 提高控制流的可读性
|
||||
|
||||
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
|
||||
|
||||
```
|
||||
if(len < 10)
|
||||
if(10 > len)
|
||||
```
|
||||
|
||||
if / else 条件语句,先处理以下逻辑:① 正逻辑;② 关键逻辑;③:简单逻辑
|
||||
```
|
||||
if(a == b) {
|
||||
// 正逻辑
|
||||
} else{
|
||||
// 反逻辑
|
||||
}
|
||||
```
|
||||
|
||||
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
|
||||
|
||||
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
|
||||
|
||||
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
|
||||
|
||||
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
|
||||
|
||||
# 第 8 章 拆分长表达式
|
||||
|
||||
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
|
||||
|
||||
```
|
||||
if line.split(':')[0].strip() == "root":
|
||||
...
|
||||
```
|
||||
```
|
||||
username = line.split(':')[0].strip()
|
||||
if username == "root":
|
||||
...
|
||||
```
|
||||
|
||||
使用摩根定理简化一些逻辑表达式:
|
||||
|
||||
```
|
||||
if(!a && !b) {
|
||||
...
|
||||
}
|
||||
```
|
||||
```
|
||||
if(a || b) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
# 第 9 章 变量与可读性
|
||||
|
||||
去除控制流变量。在循环中通过 break 或者 return 可以减少控制流变量的使用。
|
||||
|
||||
```
|
||||
boolean done = false;
|
||||
while(/* condition */ && !done) {
|
||||
...
|
||||
if(...) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
while(/* condition */) {
|
||||
...
|
||||
if(...) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
减小变量作用域。作用域越小,越容易定位到变量所有使用的地方。
|
||||
|
||||
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
|
||||
|
||||
```
|
||||
submitted = false;
|
||||
var submit_form = function(form_name) {
|
||||
if(submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
var submit_form = (function() {
|
||||
var submitted = false;
|
||||
return function(form_name) {
|
||||
if(submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
}
|
||||
}()); // () 使得外层匿名函数立即执行
|
||||
```
|
||||
|
||||
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
|
||||
|
||||
变量定义的位置应当离它使用的位置最近。
|
||||
|
||||
**实例解析**
|
||||
|
||||
在一个网页中有以下文本输入字段:
|
||||
|
||||
```
|
||||
<input type = "text" id = "input1" value = "a">
|
||||
<input type = "text" id = "input2" value = "b">
|
||||
<input type = "text" id = "input3" value = "">
|
||||
<input type = "text" id = "input4" value = "d">
|
||||
```
|
||||
|
||||
现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
|
||||
|
||||
```
|
||||
var setFirstEmptyInput = function(new_alue) {
|
||||
var found = false;
|
||||
var i = 1;
|
||||
var elem = document.getElementById('input' + i);
|
||||
while(elem != null) {
|
||||
if(elem.value === '') {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
elem = document.getElementById('input' + i);
|
||||
}
|
||||
if(found) elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
```
|
||||
|
||||
以上实现有以下问题:
|
||||
|
||||
- found 可以去除;
|
||||
- elem 作用域过大;
|
||||
- 可以用 for 循环代替 while 循环;
|
||||
|
||||
```
|
||||
var setFirstEmptyInput = function(new_value) {
|
||||
for(var i = 1; true; i++) {
|
||||
var elem = document.getElementById('input' + i);
|
||||
if(elem === null) {
|
||||
return null;
|
||||
}
|
||||
if(elem.value === '') {
|
||||
elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# 第 10 章 抽取函数
|
||||
|
||||
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
|
||||
|
||||
首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。
|
||||
|
||||
介绍性的代码:
|
||||
|
||||
```
|
||||
int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for(int i = 0; i < arr.length; i++) {
|
||||
int x = ...;
|
||||
int y = ...;
|
||||
int z = ...;
|
||||
int value = x * y * z;
|
||||
int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
|
||||
if(dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。
|
||||
|
||||
```
|
||||
public int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for(int i = 0; i < arr.length; i++) {
|
||||
int dist = computDist(arr, i);
|
||||
if(dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。
|
||||
|
||||
函数抽取也用于减小代码的冗余。
|
||||
|
||||
# 第 11 章 一次只做一件事
|
||||
|
||||
只做一件事的代码很容易让人知道其要做的事;
|
||||
|
||||
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
|
||||
|
||||
# 第 12 章 用自然语言表述代码
|
||||
|
||||
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
|
||||
|
||||
# 第 13 章 减少代码量
|
||||
|
||||
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
|
||||
|
||||
多用标准库实现。
|
@ -1,10 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 链接
|
||||
|
||||
# IO 多路复用
|
||||
|
||||
http://www.hollischuang.com/archives/1830
|
||||
|
||||
http://www.cnblogs.com/Anker/p/3265058.html
|
||||
|
@ -1,657 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第一章 概述
|
||||
|
||||
## 操作系统基本特征
|
||||
|
||||
### 1. 并发
|
||||
|
||||
并发性是指宏观上在一段时间内能同时运行多个程序,而并行性则指同一时刻能运行多个指令。
|
||||
|
||||
并行需要硬件支持,如多流水线或者多处理器。
|
||||
|
||||
操作系统通过引入进程和线程,使得程序能够并发运行。
|
||||
|
||||
### 2. 共享
|
||||
|
||||
共享是指系统中的资源可以供多个并发的进程共同使用。
|
||||
|
||||
有两种共享方式:互斥共享和同时共享。
|
||||
|
||||
互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,否则会出现错误,需要用同步机制来实现对临界资源的访问。
|
||||
|
||||
### 3. 虚拟
|
||||
|
||||
虚拟技术把一个物理实体转换为多个逻辑实体。主要有两种虚拟技术:时分复用技术和空分复用技术,例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换,这样就好像有多个处理器进行处理。
|
||||
|
||||
### 4. 异步
|
||||
|
||||
异步是指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
|
||||
|
||||
## 系统调用
|
||||
|
||||
如果一个进程在用户态需要用到操作系统的一些功能,就需要使用系统调用从而陷入内核,由操作系统代为完成。
|
||||
|
||||
可以由系统调用请求的功能有设备管理、文件管理、进程管理、进程通信、存储器管理等。
|
||||
|
||||
## 中断分类
|
||||
|
||||
### 1. 外中断
|
||||
|
||||
由 CPU 执行指令以外的事件引起,如 I/O 结束中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
|
||||
|
||||
### 2. 异常
|
||||
|
||||
由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
|
||||
|
||||
### 3. 陷入
|
||||
|
||||
在用户程序中使用系统调用。
|
||||
|
||||
## 大内核和微内核
|
||||
|
||||
### 1. 大内核
|
||||
|
||||
大内核是将操作系统功能作为一个紧密结合的整体放到内核,由于各模块共享信息,因此有很高的性能。
|
||||
|
||||
### 2. 微内核
|
||||
|
||||
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。但是需要频繁地在用户态和核心态之间进行切换,会有一定的性能损失。
|
||||
|
||||
# 第二章 进程管理
|
||||
|
||||
## 进程与线程
|
||||
|
||||
### 1. 进程
|
||||
|
||||
进程是操作系统进行资源分配的基本单位。
|
||||
|
||||
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
|
||||
|
||||
### 2. 线程
|
||||
|
||||
一个线程中可以有多个线程,是独立调度的基本单位。同一个进程中的多个线程之间可以并发执行,它们共享进程资源。
|
||||
|
||||
### 3. 区别
|
||||
|
||||
① 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问率属进程的资源。
|
||||
|
||||
② 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
|
||||
|
||||
③ 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。此外,由于同一进程内的多个线程共享进程的地址空间,因此,这些线程之间的同步与通信非常容易实现,甚至无需操作系统的干预。
|
||||
|
||||
④ 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性,而线程间可以通过直接读/写进程数据段(如全局变量)来进行通信。
|
||||
|
||||
举例:QQ 和 浏览器是两个进程,浏览器进程里面有很多线程,例如 http 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 http 请求时,浏览器还可以响应用户的其它事件。
|
||||
|
||||
## 进程状态的切换
|
||||
|
||||

|
||||
|
||||
阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU,缺少 CPU 会让进程从运行态转换为就绪态。
|
||||
|
||||
只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
|
||||
|
||||
## 调度算法
|
||||
|
||||
需要针对不同环境来讨论调度算法。
|
||||
|
||||
### 1. 批处理系统中的调度
|
||||
|
||||
#### 1.1 先来先服务(FCFS)
|
||||
|
||||
first-come first-serverd。
|
||||
|
||||
调度最先进入就绪队列的作业。
|
||||
|
||||
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
|
||||
|
||||
#### 1.2 短作业优先(SJF)
|
||||
|
||||
shortest job first。
|
||||
|
||||
调度估计运行时间最短的作业。
|
||||
|
||||
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。如果一直有短作业到来,那么长作业永远得不到调度。
|
||||
|
||||
#### 1.3 最短剩余时间优先(SRTN)
|
||||
|
||||
shortest remaining time next。
|
||||
|
||||
### 2. 交互式系统中的调度
|
||||
|
||||
#### 2.1 优先权优先
|
||||
|
||||
除了可以手动赋予优先权之外,还可以把响应比作为优先权,这种调度方式叫做高响应比优先调度算法。
|
||||
|
||||
响应比 = (等待时间 + 要求服务时间) / 要求服务时间 = 响应时间 / 要求服务时间
|
||||
|
||||
这种调度算法主要是为了解决 SJF 中长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。
|
||||
|
||||
#### 2.2 时间片轮转
|
||||
|
||||
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 分配给队首的进程。
|
||||
|
||||
时间片轮转算法的效率和时间片有很大关系。因为每次进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太短,进程切换太频繁,在进程切换上就会花过多时间。
|
||||
|
||||
#### 2.3 多级反馈队列
|
||||
|
||||

|
||||
|
||||
① 设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权越高的队列中,为每个进程所规定的执行时间片就越小。
|
||||
|
||||
② 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入下一个队列的队尾。
|
||||
|
||||
③ 仅当前 i -1 个队列均空时,才会调度第 i 队列中的进程运行。
|
||||
|
||||
优点:实时性好,也适合运行短作业和长作业。
|
||||
|
||||
#### 2.4 短进程优先
|
||||
|
||||
### 3. 实时系统中的调度
|
||||
|
||||
实时系统要一个服务请求在一个确定时间内得到响应。
|
||||
|
||||
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
|
||||
|
||||
## 进程同步
|
||||
|
||||
### 1. 临界区
|
||||
|
||||
对临界资源进行访问的那段代码称为临界区。
|
||||
|
||||
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
|
||||
|
||||
```html
|
||||
// entry section
|
||||
// critical section;
|
||||
// exit section
|
||||
```
|
||||
|
||||
### 2. 同步与互斥
|
||||
|
||||
同步指多个进程按一定顺序执行;互斥指多个进程在同一时刻只有一个进程能进入临界区。
|
||||
|
||||
同步是在对临界区互斥访问的基础上,通过其它机制来实现有序访问的。
|
||||
|
||||
### 3. 信号量
|
||||
|
||||
**信号量(Samaphore)**是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
|
||||
|
||||
- **down** : 如果信号量大于 0 ,执行 - 1 操作;如果信号量等于 0,将进程睡眠,等待信号量大于 0;
|
||||
- **up**:对信号量执行 + 1 操作,并且唤醒睡眠的进程,让进程完成 down 操作。
|
||||
|
||||
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
|
||||
|
||||
如果信号量的取值只能为 0 或者 1,那么就成为了**互斥量(Mutex)**,0 表示临界区已经加锁,1 表示临界区解锁。
|
||||
|
||||
```c
|
||||
typedef int samaphore;
|
||||
samaphore mutex = 1;
|
||||
void P1() {
|
||||
down(mutex);
|
||||
// 临界区
|
||||
up(mutex);
|
||||
}
|
||||
|
||||
void P2() {
|
||||
down(mutex);
|
||||
// 临界区
|
||||
up(mutex);
|
||||
}
|
||||
```
|
||||
|
||||
**使用信号量实现生产者-消费者问题**
|
||||
|
||||
使用一个互斥量 mutex 来对临界资源进行访问;empty 记录空缓冲区的数量,full 记录满缓冲区的数量。
|
||||
|
||||
注意,必须先执行 down 操作再用互斥量对临界区加锁,否则会出现死锁。如果都先对临界区加锁,然后再执行 down 操作,考虑这种情况:生产者对临界区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生成者睡眠。消费者此时不能进入临界区,因为生产者对临界区加锁了,也就无法对执行 up(empty) 操作,那么生产者和消费者就会一直等待下去。
|
||||
|
||||
```c
|
||||
#define N 100
|
||||
typedef int samaphore;
|
||||
samaphore mutex = 1;
|
||||
samaphore empty = N;
|
||||
samaphore full = 0;
|
||||
|
||||
void producer() {
|
||||
while(TRUE){
|
||||
int item = produce_item;
|
||||
down(empty);
|
||||
down(mutex);
|
||||
insert_item(item);
|
||||
up(mutex);
|
||||
up(full);
|
||||
}
|
||||
}
|
||||
|
||||
void consumer() {
|
||||
while(TRUE){
|
||||
down(full);
|
||||
down(mutex);
|
||||
int item = remove_item(item);
|
||||
up(mutex);
|
||||
up(empty);
|
||||
consume_item(item);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 管程
|
||||
|
||||
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
|
||||
|
||||
c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码中的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。
|
||||
|
||||
```html
|
||||
monitor ProducerConsumer
|
||||
integer i;
|
||||
condition c;
|
||||
|
||||
procedure insert();
|
||||
begin
|
||||
|
||||
end;
|
||||
|
||||
procedure remove();
|
||||
begin
|
||||
|
||||
end;
|
||||
end monitor;
|
||||
```
|
||||
|
||||
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,必须将进程阻塞,否者其它进程永远不能使用管程。
|
||||
|
||||
管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来让另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
|
||||
|
||||
**使用管程实现生成者-消费者问题**
|
||||
|
||||
```html
|
||||
monitor ProducerConsumer
|
||||
condition full, empty;
|
||||
integer count := 0;
|
||||
condition c;
|
||||
|
||||
procedure insert(item: integer);
|
||||
begin
|
||||
if count = N then wait(full);
|
||||
insert_item(item);
|
||||
count := count + 1;
|
||||
if count = 1 ten signal(empty);
|
||||
end;
|
||||
|
||||
function remove: integer;
|
||||
begin
|
||||
if count = 0 then wait(empty);
|
||||
remove = remove_item;
|
||||
count := count - 1;
|
||||
if count = N -1 then signal(full);
|
||||
end;
|
||||
end monitor;
|
||||
|
||||
procedure producer
|
||||
begin
|
||||
while true do
|
||||
begin
|
||||
item = produce_item;
|
||||
ProducerConsumer.insert(item);
|
||||
end
|
||||
end;
|
||||
|
||||
procedure consumer
|
||||
begin
|
||||
while true do
|
||||
begin
|
||||
item = ProducerConsumer.remove;
|
||||
consume_item(item);
|
||||
end
|
||||
end;
|
||||
```
|
||||
|
||||
## 进程通信
|
||||
|
||||
进程通信可以看成是不同进程间的线程通信,对于同一个进程内线程的通信方式,主要使用信号量、条件变量等同步机制。
|
||||
|
||||
### 1. 管道
|
||||
|
||||
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。
|
||||
|
||||
管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
|
||||
|
||||
Linux 中管道是通过空文件来实现。
|
||||
|
||||
管道有三种:
|
||||
|
||||
① 普通管道:有两个限制:一是只支持半双工通信方式,即只能单向传输;二是只能在父子进程之间使用;
|
||||
|
||||
② 流管道:去除第一个限制,支持双向传输;
|
||||
|
||||
③ 命名管道:去除第二个限制,可以在不相关进程之间进行通信。
|
||||
|
||||
### 2. 信号量
|
||||
|
||||
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
|
||||
|
||||
### 3. 消息队列
|
||||
|
||||
消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
|
||||
|
||||
### 4. 信号
|
||||
|
||||
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
|
||||
|
||||
### 5. 共享内存
|
||||
|
||||
共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
|
||||
|
||||
### 6. 套接字
|
||||
|
||||
套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
|
||||
|
||||
## 经典同步问题
|
||||
|
||||
生产者和消费者问题前面已经讨论过。
|
||||
|
||||
### 1. 读者-写者问题
|
||||
|
||||
允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
|
||||
|
||||
一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。
|
||||
|
||||
```c
|
||||
typedef int semaphore;
|
||||
semaphore count_mutex = 1;
|
||||
semaphore data_mutex = 1;
|
||||
int count = 0;
|
||||
|
||||
void reader() {
|
||||
while(TRUE) {
|
||||
down(count_mutex);
|
||||
count++;
|
||||
if(count == 1) down(data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
|
||||
up(count_mutex);
|
||||
read();
|
||||
down(count_mutex);
|
||||
count--;
|
||||
if(count == 0) up(data_mutex);
|
||||
up(count_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void writer() {
|
||||
while(TRUE) {
|
||||
down(data_mutex);
|
||||
write();
|
||||
up(data_mutex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 哲学家进餐问题
|
||||
|
||||

|
||||
|
||||
五个哲学家围着一张圆周,每个哲学家面前放着饭。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先一根一根拿起左右两边的筷子。
|
||||
|
||||
下面是一种错误的解法,考虑到如果每个哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。
|
||||
|
||||
```c
|
||||
#define N 5
|
||||
#define LEFT (i + N - 1) % N
|
||||
#define RIGHT (i + N) % N
|
||||
typedef int semaphore;
|
||||
semaphore chopstick[N];
|
||||
|
||||
void philosopher(int i) {
|
||||
while(TURE){
|
||||
think();
|
||||
down(chopstick[LEFT[i]]);
|
||||
down(chopstick[RIGHT[i]]);
|
||||
eat();
|
||||
up(chopstick[RIGHT[i]]);
|
||||
up(chopstick[LEFT[i]]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
为了防止死锁的发生,可以加一点限制,只允许同时拿起左右两边的筷子,方法是引入一个互斥量,对拿起两个筷子的那段代码加锁。
|
||||
|
||||
```c
|
||||
semaphore mutex = 1;
|
||||
|
||||
void philosopher(int i) {
|
||||
while(TURE){
|
||||
think();
|
||||
down(mutex);
|
||||
down(chopstick[LEFT[i]]);
|
||||
down(chopstick[RIGHT[i]]);
|
||||
up(mutex);
|
||||
eat();
|
||||
down(mutex);
|
||||
up(chopstick[RIGHT[i]]);
|
||||
up(chopstick[LEFT[i]]);
|
||||
up(mutex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 第三章 死锁
|
||||
|
||||
## 死锁的条件
|
||||
|
||||

|
||||
|
||||
1. 互斥
|
||||
2. 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
|
||||
3. 不可抢占
|
||||
4. 环路等待
|
||||
|
||||
## 死锁的处理方法
|
||||
|
||||
### 1. 鸵鸟策略
|
||||
|
||||
把头埋在沙子里,假装根本没发生问题。
|
||||
|
||||
这种策略不可取。
|
||||
|
||||
### 2. 死锁预防
|
||||
|
||||
在程序运行之前预防发生死锁。
|
||||
|
||||
#### 2.1 破坏互斥条件
|
||||
|
||||
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
|
||||
|
||||
#### 2.2 破坏请求与保持条件
|
||||
|
||||
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
|
||||
|
||||
#### 2.3 破坏不可抢占条件
|
||||
|
||||
#### 2.4 破坏环路等待
|
||||
|
||||
给资源统一编号,进程只能按编号顺序来请求资源。
|
||||
|
||||
### 3. 死锁避免
|
||||
|
||||
在程序运行时避免发生死锁。
|
||||
|
||||
#### 3.1 安全状态
|
||||
|
||||

|
||||
|
||||
图 a 的第二列 has 表示已拥有的资源数,第三列 max 表示总共需要的资源数,free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源,运行结束后释放 B,此时 free 变为 4;接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。
|
||||
|
||||
定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。
|
||||
|
||||
#### 3.2 单个资源的银行家算法
|
||||
|
||||
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
|
||||
|
||||

|
||||
|
||||
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
|
||||
|
||||
#### 3.3 多个资源的银行家算法
|
||||
|
||||

|
||||
|
||||
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
|
||||
|
||||
检查一个状态是否安全的算法如下:
|
||||
|
||||
① 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
|
||||
|
||||
② 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
|
||||
|
||||
③ 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
|
||||
|
||||
### 4. 死锁检测与死锁恢复
|
||||
|
||||
不试图组织死锁,而是当检测到死锁发生时,采取措施进行恢复。
|
||||
|
||||
#### 4.1 死锁检测算法
|
||||
|
||||
死锁检测的基本思想是,如果一个进程所请求的资源能够被满足,那么就让它执行,释放它拥有的所有资源,然后让其它能满足条件的进程执行。
|
||||
|
||||

|
||||
|
||||
上图中,有三个进程四个资源,每个数据代表的含义如下:
|
||||
|
||||
E 向量:资源总量
|
||||
A 向量:资源剩余量
|
||||
C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
|
||||
R 矩阵:每个进程请求的资源数量
|
||||
|
||||
进程 P<sub>1</sub> 和 P<sub>2</sub> 所请求的资源都得不到满足,只有进程 P<sub>3</sub> 可以,让 P<sub>3</sub> 执行,之后释放 P<sub>3</sub> 拥有的资源,此时 A = (2 2 2 0)。P<sub>1</sub> 可以执行,执行后释放 P<sub>1</sub> 拥有的资源, A = (4 2 2 2) ,P<sub>2</sub> 也可以执行。所有进程都可以顺利执行,没有死锁。
|
||||
|
||||
算法总结如下:
|
||||
|
||||
每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。
|
||||
|
||||
① 寻找一个没有标记的进程 P<sub>i</sub>,它所请求的资源小于等于 A。
|
||||
② 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 ①。
|
||||
③ 如果有没有这样一个进程,算法终止。
|
||||
|
||||
#### 4.2 死锁恢复
|
||||
|
||||
① 利用抢占恢复
|
||||
② 杀死进程
|
||||
|
||||
# 第四章 存储器管理
|
||||
|
||||
## 虚拟内存
|
||||
|
||||
每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一 页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。
|
||||
|
||||
当程序引用到一部分在物理内存中的地址空间时,由硬件立即执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
|
||||
|
||||
## 分页与分段
|
||||
|
||||
### 1. 分页
|
||||
|
||||
用户程序的地址空间被划分为若干固定大小的区域,称为“页”。相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配,由一个页表来维护它们之间的映射关系。
|
||||
|
||||
### 2. 分段
|
||||
|
||||

|
||||
|
||||
上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态递增的特点会导致覆盖问题的出现。
|
||||
|
||||

|
||||
|
||||
分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,可以动态改变。
|
||||
|
||||
每个段都需要程序员来划分。
|
||||
|
||||
### 3. 段页式
|
||||
|
||||
用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。
|
||||
|
||||
用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。程序对内存的调入或调出是按页进行的。但它又可按段实现共享和保护。
|
||||
|
||||
### 4. 分页与分段区别
|
||||
|
||||
① 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。
|
||||
|
||||
② 地址空间的维度:分页是一维地址空间,分段是二维的。
|
||||
|
||||
③ 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
|
||||
|
||||
④ 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
|
||||
|
||||
## 页面置换算法
|
||||
|
||||
在程序运行过程中,若其所要访问的页面不在内存而需要把它们调入内存,但是内存已无空闲空间时,系统必须从内存中调出一个页面到磁盘对换区中,并且将程序所需要的页面调入内存中。页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。
|
||||
|
||||
### 1. 最佳(Optimal)
|
||||
|
||||
所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。
|
||||
|
||||
是一种理论上的算法,因为无法知道一个页面多长时间会被再访问到。
|
||||
|
||||
举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:
|
||||
|
||||
7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
|
||||
|
||||
进程运行时,先将 7,0,1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。
|
||||
|
||||
### 2. 先进先出(FIFO)
|
||||
|
||||
所选择换出的页面是最先进入的页面。
|
||||
|
||||
该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。
|
||||
|
||||
### 3. 最近最久未使用(LRU,Least Recently Used)
|
||||
|
||||
虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。
|
||||
|
||||
可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶,这样,最近被访问的页面的页面号总是在栈顶,而最近最久未使用的页面的页面号总是在栈底。
|
||||
|
||||
4,7,0,7,1,0,1,2,1,2,6
|
||||
|
||||

|
||||
|
||||
### 4. 时钟(Clock)
|
||||
|
||||
Clock 页面置换算法需要用到一个访问位,当一个页面被访问时,将访问为置为 1。
|
||||
|
||||
首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0,就将该页面换出;否则将该页的访问位设置为 0,给该页面第二次的机会,移动指针继续检查。
|
||||
|
||||
# 第五章 设备管理
|
||||
|
||||
## 磁盘调度算法
|
||||
|
||||
当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。磁盘调度的主要目标是使磁盘的平均寻道时间最少。
|
||||
|
||||
### 1. 先来先服务(FCFS,First Come First Serverd)
|
||||
|
||||
根据进程请求访问磁盘的先后次序来进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
|
||||
|
||||
### 2. 最短寻道时间优先(SSTF,Shortest Seek Time First)
|
||||
|
||||
要求访问的磁道与当前磁头所在磁道距离最近的优先进行调度。这种算法并不能保证平均寻道时间最短,但是比 FCFS 好很多。
|
||||
|
||||
### 3. 扫描算法(SCAN)
|
||||
|
||||
SSTF 会出现进行饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。
|
||||
|
||||
SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求访问的磁道在磁头当前移动方向上才能够得到调度。因为考虑了移动方向,那么一个进程请求访问的磁道一定会得到调度。
|
||||
|
||||
当一个磁头自里向外移动时,移到最外侧会改变移动方向为自外向里,这种移动的规律类似于电梯的运行,因此又常称 SCAN 算法为电梯调度算法。
|
||||
|
||||
### 4. 循环扫描算法(CSCAN)
|
||||
|
||||
CSCAN 对 SCAN 进行了改动,要求磁头始终沿着一个方向移动。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014.
|
||||
|
||||
- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001.
|
||||
|
||||
- Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统.
|
||||
|
||||
- [小土刀的面试刷题笔记](http://wdxtub.com/interview/index.html)
|
||||
|
||||
- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596)
|
@ -1,21 +0,0 @@
|
||||
## Web 页面请求过程
|
||||
|
||||
1. 向 DNS 服务器发送 DNS 查询报文来解析域名。
|
||||
|
||||
2. 在运输层,DNS 查询报文会放入端口号为 53 的 UDP 数据报中。
|
||||
|
||||
3. 在网络层,UDP 数据报封装进 IP 分组中,路由器会执行路由选择算法来转发分组。
|
||||
|
||||
4. 在链路层,IP 分组会封装进链路层帧中,此时需要使用 ARP 来查询 MAC 地址,通过 ARP 得到 MAC 地址之后就可以进行链路层传输了。
|
||||
|
||||
5. 在 DNS 服务器返回域名的 IP 地址后,就可以开始进行 HTTP 会话了。
|
||||
|
||||
6. 要进行 HTTP 会话,需要建立 TCP 连接。TCP 连接的建立需要进行进行 TCP 的三次握手。TCP 连接建立后会保持持久连接。
|
||||
|
||||
7. 客户端发送 HTTP 请求报文,请求获取页面。
|
||||
|
||||
8. 如果存在缓存服务器,并且缓存服务器上有该页面,那么缓存服务器通过 HTTP 响应报文向客户端发送缓存的页面。否则,服务器发送该页面。
|
||||
|
||||
9. 浏览器得到页面内容之后,解析并渲染,向用户展示页面。
|
||||
|
||||
10. 当客户端不再需要连接时,发送的 HTTP 请求报文中包含 Connection: close 字段,从而释放 TCP 连接,TCP 连接的释放需要进行四次握手过程。
|
@ -1,760 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第一章 概述
|
||||
|
||||
## 网络的网络
|
||||
|
||||
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
|
||||
|
||||

|
||||
|
||||
## ISP
|
||||
|
||||
互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。
|
||||
|
||||
目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为主干 ISP、地区 ISP 和本地 ISP。
|
||||
|
||||
互联网交换点 IXP 允许两个网络直接相连而不用经过第三个来转发分组。
|
||||
|
||||

|
||||
|
||||
## 互联网的组成
|
||||
|
||||
1. 边缘部分:所有连接在互联网上的主机。用户可以直接使用;
|
||||
2. 核心部分:由大量的网络和连接这些网络的路由器组成。为边缘部分的主机提供服务。
|
||||
|
||||

|
||||
|
||||
## 主机之间的通信方式
|
||||
|
||||
**1. 客户 - 服务器(C/S)**
|
||||
|
||||
客户即是服务请求方,服务器是服务提供方。
|
||||
|
||||
**2. 对等(P2P)**
|
||||
|
||||
不区分客户和服务器。
|
||||
|
||||
## 电路交换与分组交换
|
||||
|
||||

|
||||
|
||||
### 1. 电路交换
|
||||
|
||||
电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。
|
||||
|
||||
### 2. 报文交换
|
||||
|
||||
报文交换用于邮局通信系统,邮局接收到一份报文之后,先存储下来,然后根据目的地选择性地把报文转发到下一个目的地,这个过程就是存储转发过程。
|
||||
|
||||
### 3. 分组交换
|
||||
|
||||
分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。
|
||||
|
||||

|
||||
|
||||
存储转发允许在一条传输线路上传送多个主机的分组,也就是分组交换不需要占用端到端的线路资源。
|
||||
|
||||
相比于报文交换,由于分组比报文更小,存储转发的速度也就更快。
|
||||
|
||||
## 时延
|
||||
|
||||
总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延
|
||||
|
||||

|
||||
|
||||
|
||||
### 1. 发送时延
|
||||
|
||||
主机或路由器发送数据帧所需要的时间。
|
||||
|
||||
$$ delay = \frac{l(bit)}{v(bit/s)} $$
|
||||
|
||||
其中 l 表示数据帧的长度,v 表示发送速率。
|
||||
|
||||
### 2. 传播时延
|
||||
|
||||
电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。
|
||||
|
||||
$$ delay = \frac{l(m)}{v(m/s)} $$
|
||||
|
||||
其中 l 表示信道长度,v 表示电磁波在信道上的传播速率。
|
||||
|
||||
### 3. 处理时延
|
||||
|
||||
主机或路由器收到分组时进行处理所需要的时间,例如分析首部,从分组中提取数据部分等。
|
||||
|
||||
### 4. 排队时延
|
||||
|
||||
分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。
|
||||
|
||||
## 计算机网络体系结构*
|
||||
|
||||

|
||||
|
||||
### 1. 七层协议
|
||||
|
||||
如图 a 所示,其中表示层和会话层用途如下:
|
||||
|
||||
1. 表示层:信息的语法语义以及它们的关联,如加密解密、转换翻译、压缩解压缩;
|
||||
2. 会话层:不同机器上的用户之间建立及管理会话。
|
||||
|
||||
### 2. 五层协议
|
||||
|
||||
1. 应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。
|
||||
|
||||
2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。
|
||||
|
||||
3. 网络层:为主机之间提供服务,而不是像运输层协议那样是为主机中的进程提供服务。网络层把运输层产生的报文段或者用户数据报封装成分组来进行传输。
|
||||
|
||||
4. 数据链路层:网络层针对的还是主机之间,而主机之间可以有很多链路,链路层协议就是为相邻结点之间提供服务。数据链路层把网络层传来的分组封装成帧。
|
||||
|
||||
5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使物理层上的数据链路层感觉不到这些差异。
|
||||
|
||||
### 3. 数据在各层之间的传递过程
|
||||
|
||||
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
|
||||
|
||||
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务。
|
||||
|
||||

|
||||
|
||||
### 4. TCP/IP 体系结构
|
||||
|
||||
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
|
||||
|
||||
现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
|
||||
|
||||

|
||||
|
||||
TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中占用举足轻重的地位。
|
||||
|
||||

|
||||
|
||||
# 第二章 物理层
|
||||
|
||||
## 通信方式
|
||||
|
||||
1. 单向通信,又称为单工通信;
|
||||
2. 双向交替通信,又称为半双工通信;
|
||||
3. 双向同时通信,又称为全双工通信。
|
||||
|
||||
## 带通调制
|
||||
|
||||
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
|
||||
|
||||

|
||||
|
||||
## 信道复用技术
|
||||
|
||||
### 1. 频分复用、时分复用
|
||||
|
||||
频分复用的所有用户在相同的时间占用不同的频率带宽资源;时分复用的所有用户在不同的时间占用相同的频率带宽资源。
|
||||
|
||||
使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
|
||||
|
||||

|
||||
|
||||
### 2. 统计时分复用
|
||||
|
||||
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成时分复用帧然后发送。
|
||||
|
||||

|
||||
|
||||
### 3. 波分复用
|
||||
|
||||
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
|
||||
|
||||

|
||||
|
||||
### 4. 码分复用
|
||||
|
||||
为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 $\vec{S}$ 和 $\vec{T}$ 有
|
||||
|
||||
$$ \vec{S} \cdot \vec{T} = 0 $$
|
||||
|
||||
为了方便,取 m=8,设码片 $\vec{S}$ 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
|
||||
|
||||
在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到
|
||||
|
||||
$$ \frac{1}{m} \vec{S} \cdot \vec{S} = 1 $$
|
||||
|
||||
$$ \frac{1}{m} \vec{S} \cdot \vec{S'} = -1 $$
|
||||
|
||||
其中 $\vec{S'}$ 为 $\vec{S}$ 的反码。
|
||||
|
||||
利用上面的式子我们知道,当接收端使用码片 $\vec{S}$ 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。
|
||||
|
||||
码分复用发送的数据量为原先的 m 倍。
|
||||
|
||||

|
||||
|
||||
# 第三章 数据链路层
|
||||
|
||||
## 三个基本问题
|
||||
|
||||
### 1. 封装成帧
|
||||
|
||||
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
|
||||
|
||||

|
||||
|
||||
### 2. 透明传输
|
||||
|
||||
透明表示一个实际存在的事物看起来好像不存在一样。
|
||||
|
||||
帧中有首部和尾部,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据中出现首部尾部相同的内容前面插入转义字符,如果需要传输的内容正好就是转义字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户很难察觉到转义字符的存在。
|
||||
|
||||

|
||||
|
||||
### 3. 差错检测
|
||||
|
||||
目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。
|
||||
|
||||
## 点对点信道 -PPP 协议
|
||||
|
||||
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议就是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
|
||||
|
||||

|
||||
|
||||
在 PPP 的帧中,F 字段为帧的定界符,A 和 C 暂时没有意义。FCS 是使用 CRC 的检验序列。信息字段的长度不超过 1500。
|
||||
|
||||

|
||||
|
||||
## 局域网的拓扑
|
||||
|
||||

|
||||
|
||||
## 广播信道 - CSMA/CD 协议*
|
||||
|
||||
在广播信道上,同一时间只能允许一台计算机发送数据。
|
||||
|
||||
CSMA/CD 表示载波监听多点接入 / 碰撞检测。
|
||||
|
||||
**多点接入**:说明这是总线型网络,许多计算机以多点的方式连接到总线上。
|
||||
**载波监听**:每个站都必须不停地检听信道。在发送前,如果检听信道正在使用,就必须等待。
|
||||
**碰撞检测**:在发送中,如果检听信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经检听信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
|
||||
|
||||

|
||||
|
||||
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期**。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
|
||||
|
||||
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定,从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。
|
||||
|
||||
## 集线器
|
||||
|
||||
从表面上看,使用集线器的局域网在物理上是一个星型网。但是集线器使用电子器件来模拟实际缆线的工作,逻辑上仍是一个总线网,整个系统仍像一个传统以太网那样运行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## MAC 层
|
||||
|
||||
MAC 地址是 6 字节(48 位)的地址,用于唯一表示适配器,一台主机拥有多个适配器就有多个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。
|
||||
|
||||
MAC 帧用类型字段来标记上层使用什么协议;数据字段长度在 46-1500 之间,如果太小则需要填充;FCS 为帧检验序列,使用的是 CRC 检验方法;前面插入的前同步码只是为了计算 FCS 临时加入的,计算结束之后会丢弃。
|
||||
|
||||

|
||||
|
||||
## 虚拟局域网
|
||||
|
||||
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
|
||||
|
||||

|
||||
|
||||
# 第四章 网络层*
|
||||
|
||||
## 网际协议 IP 概述
|
||||
|
||||
因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。
|
||||
|
||||
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
|
||||
|
||||

|
||||
|
||||
与 IP 协议配套使用的还有三个协议:
|
||||
|
||||
1. 地址解析协议 ARP(Address Resolution Protocol)
|
||||
2. 网际控制报文协议 ICMP(Internet Control Message Protocol)
|
||||
3. 网际组管理协议 IGMP(Internet Group Management Protocol)
|
||||
|
||||

|
||||
|
||||
## IP 数据报格式
|
||||
|
||||

|
||||
|
||||
**版本** : 有 4(IPv4)和 6(IPv6)两个值;
|
||||
|
||||
**首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选部分的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
|
||||
|
||||
**区分服务** : 用来获得更好的服务,一般情况下不适用。
|
||||
|
||||
**总长度** : 包括首部长度和数据部分长度。
|
||||
|
||||
**标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
|
||||
|
||||
**片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
|
||||
|
||||

|
||||
|
||||
**生存时间** :TTL,它的存在为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。
|
||||
|
||||
**协议**:指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
|
||||
|
||||
**首部检验和**:因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
|
||||
|
||||
## IP 地址编址
|
||||
|
||||
IP 地址的编址方式经历了三个历史阶段:
|
||||
|
||||
1. 分类的 IP 地址;
|
||||
2. 子网的划分;
|
||||
3. 构成超网。
|
||||
|
||||
### 1. 分类的 IP 地址
|
||||
|
||||
由两部分组成,网络号和主机号,其中不同类别具有不同的网络号长度,并且是固定的。
|
||||
|
||||
IP 地址 ::= {< 网络号 >, < 主机号 >}
|
||||
|
||||

|
||||
|
||||
### 2. 划分子网
|
||||
|
||||
通过在网络号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。
|
||||
|
||||
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
|
||||
|
||||
要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 000000,也就是 255.255.192.0。
|
||||
|
||||
### 3. 无分类编址 CIDR(构成超网)
|
||||
|
||||
CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
|
||||
|
||||
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}
|
||||
|
||||
CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。
|
||||
|
||||
CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。
|
||||
|
||||
一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为构成超网。
|
||||
|
||||
在路由表中每个项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配。
|
||||
|
||||
## IP 地址和 MAC 地址
|
||||
|
||||
网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
|
||||
|
||||

|
||||
|
||||
|
||||
## 地址解析协议 ARP
|
||||
|
||||
实现由 IP 地址得到 MAC 地址。
|
||||
|
||||
每个主机都有一个 ARP 高速缓存,存放映射表。如果一个 IP 地址 到 MAC 地址的映射不在该表中,主机通过广播的方式发送 ARP 请求分组,匹配 IP 地址的主机会发送 ARP 响应分组告知 MAC 地址。
|
||||
|
||||

|
||||
|
||||
## 路由器的结构
|
||||
|
||||
路由器可以划分为两大部分:路由选择和分组转发。
|
||||
|
||||
分组转发部分由三部分组成:交换结构、一组输入端口和一组输出端口。
|
||||
|
||||

|
||||
|
||||
交换结构的交换网络有以下三种实现方式:
|
||||
|
||||

|
||||
|
||||
## 交换机与路由器的区别
|
||||
|
||||
- 交换机工作于数据链路层,能识别 MAC 地址,根据 MAC 地址转发链路层数据帧。具有自学机制来维护 IP 地址与 MAC 地址的映射。
|
||||
|
||||
- 路由器位于网络层,能识别 IP 地址并根据 IP 地址转发分组。维护着路由表,根据路由表选择最佳路线。
|
||||
|
||||
## 路由器分组转发流程
|
||||
|
||||
1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。(路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量)
|
||||
2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
|
||||
3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
|
||||
4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
|
||||
5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
|
||||
6. 报告转发分组出错。
|
||||
|
||||

|
||||
|
||||
## 路由选择协议
|
||||
|
||||
互联网使用的路由选择协议都是自适应的,能随着网络通信量和拓扑变化而自适应地进行调整。
|
||||
|
||||
互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。
|
||||
|
||||
可以把路由选择协议划分为两大类:
|
||||
|
||||
1. 内部网关协议 IGP(Interior Gateway Protocol) 在自治系统内部使用,如 RIP 和 OSPF。
|
||||
2. 外部网关协议 EGP(External Gateway Protocol) 在自治系统之间使用,如 BGP。
|
||||
|
||||

|
||||
|
||||
### 1. 内部网关协议 RIP
|
||||
|
||||
RIP 是一种分布式的基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1,跳数最多为 15,超过 15 表示不可达。
|
||||
|
||||
RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。
|
||||
|
||||
距离向量算法:
|
||||
|
||||
1. 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1;
|
||||
2. 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
|
||||
- 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中;
|
||||
- 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。
|
||||
3. 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。
|
||||
|
||||
RIP 协议实现简单,开销小,但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。
|
||||
|
||||
### 2. 内部网关协议 OSPF
|
||||
|
||||
开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。
|
||||
|
||||
开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先是因为使用了 Dijkstra 提出的最短路径算法 SPF。
|
||||
|
||||
OSPF 具有以下特点:
|
||||
|
||||
1. 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
|
||||
2. 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
|
||||
3. 只有当链路状态发生变化时,路由器才会发送信息。
|
||||
|
||||
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。
|
||||
|
||||
### 3. 外部网关协议 BGP
|
||||
|
||||
AS 之间的路由选择很困难,主要是互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。
|
||||
|
||||
BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路径向量路由选择协议。
|
||||
|
||||
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
|
||||
|
||||

|
||||
|
||||
## 网际控制报文协议 ICMP
|
||||
|
||||
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
|
||||
|
||||

|
||||
|
||||
ICMP 报文分为差错报告报文和询问报文。
|
||||
|
||||

|
||||
|
||||
## 分组网间探测 PING
|
||||
|
||||
PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
|
||||
|
||||
PING 的过程:
|
||||
|
||||
1. PING 同一个网段的主机,查找目的主机的 MAC 地址,然后直接交付。如果无法查找到 MAC 地址,就要进行一次 ARP 请求。
|
||||
2. PING 不同网段的主机,就发送给网关让其进行转发。同样要发送给网关也需要通过查找网关的 MAC 地址,根据 MAC 地址进行转发。
|
||||
|
||||
## IP 多播
|
||||
|
||||
在一对多的通信中,多播不需要将分组复制多份,从而大大节约网络资源。
|
||||
|
||||

|
||||
|
||||
## 虚拟专用网 VPN
|
||||
|
||||
由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。
|
||||
|
||||
有三个专用地址块:
|
||||
|
||||
1. 10.0.0.0 ~ 10.255.255.255
|
||||
2. 172.16.0.0 ~ 172.31.255.255
|
||||
3. 192.168.0.0 ~ 192.168.255.255
|
||||
|
||||
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。
|
||||
|
||||
下图中,场所 A 和 B 的通信部经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
|
||||
|
||||

|
||||
|
||||
## 网络地址转换 NAT
|
||||
|
||||
专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。
|
||||
|
||||
在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,这样可以使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
|
||||
|
||||

|
||||
|
||||
# 第五章 运输层*
|
||||
|
||||
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。
|
||||
|
||||
运输层提供了应用进程间的逻辑通信。运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看见的好像在两个运输层实体之间有一条端到端的逻辑通信信道。
|
||||
|
||||
## UDP 和 TCP 的特点
|
||||
|
||||
用户数据包协议 UDP(User Datagram Protocol)
|
||||
传输控制协议 TCP(Transmission Control Protocol)
|
||||
|
||||
UDP 是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部)。
|
||||
|
||||
TCP 是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)
|
||||
|
||||
## UDP 首部格式
|
||||
|
||||

|
||||
|
||||
首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和而临时添加的。
|
||||
|
||||
## TCP 首部格式
|
||||
|
||||

|
||||
|
||||
**序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
|
||||
|
||||
**确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
|
||||
|
||||
**数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
|
||||
|
||||
**确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
|
||||
|
||||
**同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
|
||||
|
||||
**终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。
|
||||
|
||||
**窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
|
||||
|
||||
## TCP 的三次握手
|
||||
|
||||

|
||||
|
||||
假设 A 为客户端,B 为服务器端。
|
||||
|
||||
1. 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
|
||||
2. A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。
|
||||
3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
|
||||
4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。
|
||||
5. B 收到 A 的确认后,连接建立。
|
||||
|
||||
|
||||
## TCP 的四次挥手
|
||||
|
||||

|
||||
|
||||
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
|
||||
|
||||
1. A 发送连接释放报文段,FIN=1;
|
||||
2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据;
|
||||
3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1;
|
||||
4. A 收到后发出确认,此时连接释放。
|
||||
|
||||
**TIME_WAIT**
|
||||
|
||||
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间。这么做有两个理由:
|
||||
|
||||
1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。
|
||||
2. 连接过程可能“已失效的连接请求报文段”,为了防止这种报文段出现在本次连接之外,需要等待一段时间。
|
||||
|
||||
## TCP 滑动窗口
|
||||
|
||||

|
||||
|
||||
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
|
||||
|
||||
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
|
||||
|
||||
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 32, 34, 35},其中 {31, 32} 按序到达,而 {34, 35} 就不是,因此只对字节 32 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
|
||||
|
||||
## TCP 可靠传输
|
||||
|
||||
TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
|
||||
|
||||
一个报文段从发送到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:
|
||||
|
||||
$$ RTTs = (1-a)*(RTTs) + a*RTT $$
|
||||
|
||||
可以知道,超时时间 RTO 应该略大于 RRTs,TCP 使用的超时时间计算如下:
|
||||
|
||||
$$ RTO = RTTs + 4*RTT_d $$
|
||||
|
||||
其中 RTT<sub>d</sub> 为偏差,它与新的 RRT 和 RRTs 有关。
|
||||
|
||||
## TCP 流量控制
|
||||
|
||||
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
|
||||
|
||||
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。例如将窗口字段设置为 0,则发送方不能发送数据。
|
||||
|
||||
## TCP 拥塞控制
|
||||
|
||||
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。
|
||||
|
||||

|
||||
|
||||
TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护有一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
|
||||
|
||||
为了便于讨论,做如下假设:
|
||||
|
||||
1. 接收方有足够大的接收缓存,因此不会发生流量控制;
|
||||
2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
|
||||
|
||||

|
||||
|
||||
### 慢开始与拥塞避免
|
||||
|
||||
发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段为:2、4、8 ...
|
||||
|
||||
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
|
||||
|
||||
如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
|
||||
|
||||
### 快重传与快恢复
|
||||
|
||||
在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认,例如已经接收到 M<sub>1</sub> 和 M<sub>2</sub>,此时收到 M<sub>4</sub>,应当发送对 M<sub>2</sub> 的确认。
|
||||
|
||||
在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失,例如收到三个 M<sub>2</sub> ,则 M<sub>3</sub> 丢失。此时执行快重传,立即重传下一个报文段。
|
||||
|
||||
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
|
||||
|
||||

|
||||
|
||||
# 第六章 应用层*
|
||||
|
||||
## 域名系统 DNS
|
||||
|
||||
把主机名解析为 IP 地址。
|
||||
|
||||
被设计成分布式系统。
|
||||
|
||||
### 1. 层次结构
|
||||
|
||||
一个域名由多个层次构成,从上层到下层分别为顶级域名、二级域名、三级域名以及四级域名。所有域名可以画成一颗域名树。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
域名服务器可以分为以下四类:
|
||||
|
||||
**(1) 根域名服务器**:解析顶级域名;
|
||||
|
||||
**(2) 顶级域名服务器**:解析二级域名;
|
||||
|
||||
**(3) 权限域名服务器**:解析区内的域名;
|
||||
|
||||
区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区:abc.com 和 y.abc.com
|
||||
|
||||

|
||||
|
||||
因此就需要两个权限域名服务器:
|
||||
|
||||

|
||||
|
||||
**(4) 本地域名服务器**:也称为默认域名服务器。可以在其中配置高速缓存。
|
||||
|
||||
### 2. 解析过程
|
||||
|
||||
主机向本地域名服务器解析的过程采用递归,而本地域名服务器向其它域名服务器解析可以使用递归和迭代两种方式。
|
||||
|
||||
迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归地方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。
|
||||
|
||||

|
||||
|
||||
## 文件传输协议 FTP
|
||||
|
||||
FTP 在运输层使用 TCP,并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21,数据连接使用端口号 20。
|
||||
|
||||

|
||||
|
||||
## 远程终端协议 TELNET
|
||||
|
||||
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
|
||||
|
||||
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
|
||||
|
||||
## 万维网 WWW
|
||||
|
||||
见 HTTP 笔记。
|
||||
|
||||
## 电子邮件协议
|
||||
|
||||
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件发送协议和读取协议。其中发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。
|
||||
|
||||

|
||||
|
||||
### POP3
|
||||
|
||||
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
|
||||
|
||||
### IMAP
|
||||
|
||||
IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。IMAP 协议也支持创建自定义的文件夹。
|
||||
|
||||
### SMTP
|
||||
|
||||
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主题的结构,定义了非 ASCII 码的编码规则。
|
||||
|
||||

|
||||
|
||||
## 动态主机配置协议 DHCP
|
||||
|
||||
DHCP 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。
|
||||
|
||||
DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、默认路由器 IP 地址、域名服务器的 IP 地址。
|
||||
|
||||
工作方式如下:需要 IP 地址的主机广播发送 DHCP 发现报文(将目的地址置为全 1,即 255.255.255.255:67,源地址设置为全 0,即 0.0.0.0:68),DHCP 服务器收到发现报文之后,则在 IP 地址池中取一个地址,发送 DHCP 提供报文给该主机。
|
||||
|
||||
## 点对点传输 P2P
|
||||
|
||||
把某个文件分发的所有对等集合称为一个洪流。文件的数据单元称为文件块,它的大小是固定的。一个新的对等方加入某个洪流,一开始并没有文件块,但是能够从其它对等方中逐渐地下载到一些文件块,与此同时,它也为别的对等方上传一些文件块。
|
||||
|
||||
每个洪流都有一个基础设施,称为追踪器。当一个对等方加入洪流时,必须向追踪器登记,并周期性地通知追踪器它仍在洪流中。可以在任何时间加入和退出某个洪流。
|
||||
|
||||
一个新的对等方加入洪流时,追踪器会随机从洪流中选择若干个对等方,并让新对等方与这些对等方建立连接,把这些对等方称为相邻对等方。接收和发送文件块都是在相邻对等方中进行。
|
||||
|
||||
当一个对等方需要很多文件块时,通过使用最稀有优先的策略来取得文件块,也就是一个文件块在相邻对等方中副本最少,那么就优先请求这个文件块。
|
||||
|
||||
当很多对等方向同一个对等方请求文件块时,该对等方优先选择以最高速率向其发送文件块的对等方。
|
||||
|
||||
P2P 是一个分布式系统,任何时候都有对等方加入或者退出。使用分布式散列表 DHT,可以查找洪流中的资源和 IP 地址映射。
|
||||
|
||||
## Web 页面请求过程
|
||||
|
||||
1. 向 DNS 服务器发送 DNS 查询报文来解析域名。
|
||||
|
||||
2. 开始进行 HTTP 会话,需要先建立 TCP 连接。
|
||||
|
||||
3. 在运输层的传输过程中,HTTP 报文被封装进 TCP 中。HTTP 请求报文使用端口号 80,因为服务器监听的是 80 端口。连接建立之后,服务器会随机分配一个端口号给特定的客户端,之后的 TCP 传输都是用这个分配的端口号。
|
||||
|
||||
4. 在网络层的传输过程中,TCP 报文段会被封装进 IP 分组中,IP 分组经过路由选择,最后到达目的地。
|
||||
|
||||
5. 在链路层,IP 分组会被封装进 MAC 帧中,IP 地址解析成 MAC 地址需要使用 ARP。
|
||||
|
||||
6. 客户端发送 HTTP 请求报文,请求获取页面。
|
||||
|
||||
7. 服务器发送 HTTP 相应报文,客户端从而获取该页面。
|
||||
|
||||
8. 浏览器得到页面内容之后,解析并渲染,向用户展示页面。
|
||||
|
||||
|
||||
## 常用端口
|
||||
|
||||
| 应用层协议 | 端口号 | 运输层协议 |
|
||||
| -- | -- | -- |
|
||||
| DNS | 53 | UDP |
|
||||
| FTP | 控制连接 21,数据连接 20 | TCP |
|
||||
| TELNET | 23 | TCP |
|
||||
| DHCP | 67 68 | UDP |
|
||||
| HTTP | 80 | TCP |
|
||||
| SMTP | 25 | TCP |
|
||||
| POP3 | 110 | TCP |
|
||||
| IMAP | 143 | TCP |
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 计算机网络 第七版
|
||||
- 自顶向下计算机网络
|
1703
notes/笔记/设计模式.md.txt
1703
notes/笔记/设计模式.md.txt
File diff suppressed because it is too large
Load Diff
@ -1,877 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 第一章 第一个案例
|
||||
|
||||
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构这个程序。
|
||||
|
||||
在重构前,需要先构建好可靠的测试环境,确保安全地重构。
|
||||
|
||||
重构是以微小的步伐修改程序,如果犯下错误,很容易便可以发现它。
|
||||
|
||||
**案例分析**
|
||||
|
||||
影片出租店应用程序,包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。
|
||||
|
||||

|
||||
|
||||
最开始的实现是把所有的计费代码都放在 Customer 类中,在变化发生时,需要对这部分代码进行更改。本案例中可能发生的变化有:一种类别的计费方式发生改变;添加新的电影类别。考虑到计费代码可能存在于多处,一旦发生改变时,就需要对所有计费代码进行修改。
|
||||
|
||||

|
||||
|
||||
以下是继承 Movie 的多态方案。但是由于一部 Movie 的类别会动态改变,因此这种方案不可行。
|
||||
|
||||

|
||||
|
||||
引入 Price 来反应类别信息,通过组合的方式在 Movie 中加入 Price 对象,这样每种类别的计费方式都封装在不同的 Price 子类中,并且 Movie 对象也可以动态改变类别。这种方式可以很好地适应上述提到的变化。
|
||||
|
||||

|
||||
|
||||
重构后的时序图和类图:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 第二章 重构原则
|
||||
|
||||
重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
|
||||
|
||||
重构的好处:改进软件设计;使软件更容易理解;帮助找到 bug;提高编程速度。
|
||||
|
||||
三次法则:第一次做某件事时只管去做;第二次做类似事情时可以去做;第三次再做类似的事,就应该重构。
|
||||
|
||||
间接层与重构:计算机科学中的很多问题可以通过增加一个间接层来解决,间接层具有以下价值:允许逻辑共享;分开解释意图和实现;隔离变化;封装条件逻辑。重构可以理解为在适当的位置插入间接层以及在不需要时移除间接层。
|
||||
|
||||
修改接口:可以保留旧接口,让旧接口去调用新接口,并且使用 Java 提供的 @deprecation 将旧接口标记为弃用。除非真有必要,不要发布接口,并且不要过早发布接口。
|
||||
|
||||
当现有代码过于混乱时,应当重写而不是重构。一个折中的办法是,将代码封装成一个个组件,然后对各个组件做重写或者重构的决定。
|
||||
|
||||
软件开发无法预先设计,因为开发过程有很多变化发生,在最开始不可能都把所有情况考虑进去。重构可以简化设计,重构在一个简单的设计上进行修修改改,当变化发生时,以一种灵活的方式去应对变化,进而带来更好的设计。
|
||||
|
||||
为了软代码更容易理解,重构可能会导致性能减低。在编写代码时,不用对性能过多关注,只有在最后性能优化阶段再考虑性能问题。应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。
|
||||
|
||||
# 第三章 代码的坏味道
|
||||
|
||||
## 1. Duplicated Code(重复代码)
|
||||
|
||||
同一个类的两个函数有相同表达式,则用 Extract Method 提取出重复代码;
|
||||
|
||||
两个互为兄弟的子类含有相同的表达式,先使用 Extract Method,然后把提取出来的函数 Pull Up Method 推入超类。
|
||||
|
||||
如果只是部分相同,用 Extract Method 分离出相似部分和差异部分,然后使用 Form Template Method 这种模板方法设计模式。
|
||||
|
||||
如果两个毫不相关的类出现重复代码,则使用 Extract Class 方法将重复代码提取到一个独立类中。
|
||||
|
||||
## 2. Long Method(过长函数)
|
||||
|
||||
间接层的价值:解释能力、共享能力、选择能力;
|
||||
|
||||
分解函数的原则:当需要用注释来说明一段代码时,就需要把这部分代码写入一个独立的函数中。
|
||||
|
||||
Extract Method 会把很多参数和临时变量都当做参数,可以用 Replace Temp with Query 消除临时变量,Introduce Parameter Object 和 Preserve Whole Object 可以将过长的参数列变得更简洁。
|
||||
|
||||
条件和循环往往也需要提取到新的函数中。
|
||||
|
||||
## 3. Large Class(过大的类)
|
||||
|
||||
过大的类做了过多事情,需要使用 Extract Class 或 Extract Subclass。
|
||||
|
||||
先确定客户端如何使用它们,然后运用 Extract Interface 为每一种使用方式提取出一个接口。
|
||||
|
||||
## 4. Long Parameter List(过长的参数列)
|
||||
|
||||
## 5. Divergent Change(发散式变化)
|
||||
|
||||
一个类受到多种变化的影响;
|
||||
|
||||
针对某种原因的变化,使用 Extract Class 将它提炼到一个类中。
|
||||
|
||||
## 6. Shotgun Surgery(散弹式修改)
|
||||
|
||||
一个变化引起多个类修改;
|
||||
|
||||
使用 Move Method 和 Move Field 把所有需要修改地代码放到同一个类中。
|
||||
|
||||
## 7. Feature Envy(依恋情结)
|
||||
|
||||
一个函数对某个类的兴趣高于对自己所处类的兴趣,通常是过多访问其它类的数据。
|
||||
|
||||
使用 Move Method 将它移到该去的地方,如果对多个类都有 Feature Envy,先用 Extract Method 提取出多个函数。
|
||||
|
||||
## 8. Data Clumps(数据泥团)
|
||||
|
||||
有些数据经常一起出现,比如两个类具有相同的字段、许多函数有相同的参数。使用 Extract Class 将它们放在一起。
|
||||
|
||||
## 9. Primitive Obsession(基本类型偏执)
|
||||
|
||||
使用类往往比使用基本类型更好,使用 Replace Data Value with Object 将数据值替换为对象。
|
||||
|
||||
## 10. Switch Statements(switch 惊悚现身)
|
||||
|
||||
## 11. Parallel Inheritance Hierarchies(平行继承体系)
|
||||
|
||||
每当为某个类增加一个子类,必须也为另一个类相应增加一个子类。
|
||||
|
||||
这种结果会带来一些重复性,消除重复性的一般策略:让一个继承体系的实例引用另一个继承体系的实例。
|
||||
|
||||
## 12. Lazy Class(冗余类)
|
||||
|
||||
如果一个类没有做足够多的工作,就应该消失。
|
||||
|
||||
## 13. Speculative Generality(夸夸其谈未来性)
|
||||
|
||||
有些内容是用来处理未来可能发生的变化,但是往往会造成系统难以理解和维护,并且预测未来可能发生的改变很可能和最开始的设想相反。因此,如果不是必要,就不要这么做。
|
||||
|
||||
## 14. Temporary Field(令人迷惑的暂时字段)
|
||||
|
||||
某个字段仅为某种特定情况而设,这样的代码不易理解,因为通常认为对象在所有时候都需要它的所有字段。
|
||||
|
||||
把这种字段和特定情况的处理操作使用 Extract Class 提炼到一个独立类中。
|
||||
|
||||
## 15. Message Chains(过度耦合的消息链)
|
||||
|
||||
一个对象请求另一个对象,然后再向后者请求另一个对象,然后...,这就是消息链。采用这种方式,意味着客户代码将与对象间的关系紧密耦合。
|
||||
|
||||
改用函数链,用函数委托另一个对象来处理。
|
||||
|
||||
## 16. Middle Man(中间人)
|
||||
|
||||
中间人负责处理委托给它的操作,如果一个类中有过多的函数都委托给其它类,那就是过度运用委托,应当 Remove Middle Man,直接与负责的对象打交道。
|
||||
|
||||
## 17. Inappropriate Intimacy(狎昵关系)
|
||||
|
||||
两个类多于亲密,花费太多时间去探讨彼此的 private 成分。
|
||||
|
||||
## 18. Alernative Classes with Different Interfaces(异曲同工的类)
|
||||
|
||||
## 19. Incomplete Library Class(不完美的类库)
|
||||
|
||||
类库的设计者不可能设计出完美的类库,当我们需要对类库进行一些修改时,可以使用以下两种方法:如果只是修改一两个函数,使用 Introduce Foreign Method;如果要添加一大堆额外行为,使用 Introduce Local Extension。
|
||||
|
||||
## 20. Data Class(幼稚的数据类)
|
||||
|
||||
它只拥有一些数据字段。
|
||||
|
||||
找出字段使用的地方,然后把相应的操作移到 Data Class 中。
|
||||
|
||||
## 21. Refused Bequest(被拒绝的馈赠)
|
||||
|
||||
子类继承超类的所有函数和数据,但是它只想要一部分。
|
||||
|
||||
为子类新建一个兄弟类,不需要的函数或数据使用 Push Down Method 和 Push Down Field 下推给那个兄弟。
|
||||
|
||||
## 22. Comments(过多的注释)
|
||||
|
||||
使用 Extract Method 提炼出需要注释的部分,然后用函数名来解释函数的行为。
|
||||
|
||||
# 第四章 构筑测试体系
|
||||
|
||||
Java 可以使用 Junit 进行单元测试。
|
||||
|
||||
单元测试的对象是类的方法,而功能测以客户的角度保证软件正常运行。
|
||||
|
||||
应当集中测试可能出错的边界条件。
|
||||
|
||||
# 第五章 重构列表
|
||||
|
||||
小步前进,频繁测试。
|
||||
|
||||
# 第六章 重新组织函数
|
||||
|
||||
## 1. Extract Method(提炼函数)
|
||||
|
||||
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
|
||||
|
||||
## 2. Inline Method(内联函数)
|
||||
|
||||
一个函数的本体与名称同样清楚易懂。
|
||||
|
||||
在函数调用点插入函数本体,然后移除该函数。
|
||||
|
||||
## 3. Inline Temp(内联临时变量)
|
||||
|
||||
一个临时变量,只被简单表达式赋值一次,而它妨碍了其它重构手法。
|
||||
|
||||
将所有对该变量的引用替换为对它赋值的那个表达式自身。
|
||||
|
||||
```java
|
||||
double basePrice = anOrder.basePrice();
|
||||
return basePrice > 1000;
|
||||
```
|
||||
|
||||
```java
|
||||
return anOrder.basePrice() > 1000;
|
||||
```
|
||||
|
||||
## 4. Replace Temp with Query(以查询取代临时变量)
|
||||
|
||||
以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。
|
||||
|
||||
```java
|
||||
double basePrice = quantity * itemPrice;
|
||||
if(basePrice > 1000)
|
||||
return basePrice * 0.95;
|
||||
else
|
||||
return basePrice * 0.98;
|
||||
```
|
||||
|
||||
```java
|
||||
if(basePrice() > 1000)
|
||||
return basePrice() * 0.95;
|
||||
else
|
||||
return basePrice() * 0.98;
|
||||
|
||||
// ...
|
||||
double basePrice(){
|
||||
return quantity * itemPrice;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Introduce Explaining Variable(引起解释变量)
|
||||
|
||||
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
|
||||
|
||||
```java
|
||||
if((platform.toUpperCase().indexOf("MAC") > -1) &&
|
||||
(browser.toUpperCase().indexOf("IE") > -1) &&
|
||||
wasInitialized() && resize > 0) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
|
||||
final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1;
|
||||
final boolean wasResized = resize > 0;
|
||||
|
||||
if(isMacOS && isIEBrower && wasInitialized() && wasResized) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Split Temporary Variable(分解临时变量)
|
||||
|
||||
某个临时变量被赋值超过一次,它既不是循环变量,也不是用于收集计算结果。
|
||||
|
||||
针对每次赋值,创造一个独立、对应的临时变量,每个临时变量只承担一个责任。
|
||||
|
||||
## 7. Remove Assigments to Parameters(移除对参数的赋值)
|
||||
|
||||
以一个临时变量取代对该参数的赋值。
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
if (inputVal > 50) inputVal -= 2;
|
||||
```
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
int result = inputVal;
|
||||
if (inputVal > 50) result -= 2;
|
||||
```
|
||||
|
||||
## 8. Replace Method with Method Object(以函数对象取代函数)
|
||||
|
||||
当对一个大型函数采用 Extract Method 时,由于包含了局部变量使得很难进行该操作。
|
||||
|
||||
将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后可以在同一个对象中将这个大型函数分解为多个小型函数。
|
||||
|
||||
## 9. Subsititute Algorithn(替换算法)
|
||||
|
||||
# 第七章 在对象之间搬移特性
|
||||
|
||||
## 1. Move Method(搬移函数)
|
||||
|
||||
类中的某个函数与另一个类进行更多交流:调用后者或者被后者调用。
|
||||
|
||||
将这个函数搬移到另一个类中。
|
||||
|
||||
## 2. Move Field(搬移字段)
|
||||
|
||||
类中的某个字段被另一个类更多地用到,这里的用到是指调用取值设值函数,应当把该字段移到另一个类中。
|
||||
|
||||
## 3. Extract Class(提炼类)
|
||||
|
||||
某个类做了应当由两个类做的事。
|
||||
|
||||
应当建立一个新类,将相关的字段和函数从旧类搬移到新类。
|
||||
|
||||
## 4. Inline Class(将类内联化)
|
||||
|
||||
与 Extract Class 相反。
|
||||
|
||||
## 5. Hide Delegate(隐藏“委托关系”)
|
||||
|
||||
建立所需的函数,隐藏委托关系。
|
||||
|
||||
```java
|
||||
class Person{
|
||||
Department department;
|
||||
|
||||
public Department getDepartment(){
|
||||
return department;
|
||||
}
|
||||
}
|
||||
|
||||
class Department{
|
||||
private Person manager;
|
||||
|
||||
public Person getManager(){
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果客户希望知道某人的经理是谁,必须获得 Department 对象,这样就对客户揭露了 Department 的工作原理。
|
||||
|
||||
```java
|
||||
Person manager = john.getDepartment().getManager();
|
||||
```
|
||||
|
||||
通过为 Peron 建立一个函数来隐藏这种委托关系。
|
||||
|
||||
```java
|
||||
public Person getManager(){
|
||||
return department.getManager();
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Remove Middle Man(移除中间人)
|
||||
|
||||
与 Hide Delegate 相反,本方法需要移除委托函数,让客户直接调用委托类。
|
||||
|
||||
Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受托类的新特性时,就必须在服务器端添加一个简单的委托函数。随着受委托的特性越来越多,服务器类完全变成了一个“中间人”。
|
||||
|
||||
## 7. Introduce Foreign Method(引入外加函数)
|
||||
|
||||
需要为提供服务的类添加一个函数,但是无法修改这个类。
|
||||
|
||||
可以在客户类中建立一个函数,并以第一参数形式传入一个服务类的实例,让客户类组合服务器实例。
|
||||
|
||||
## 8. Introduce Local Extension(引入本地扩展)
|
||||
|
||||
和 Introduce Foreign Method 目的一样,但是 Introduce Local Extension 通过建立新的类来实现。有两种方式:子类或者包装类,子类就是通过继承实现,包装类就是通过组合实现。
|
||||
|
||||
# 第八章 重新组织数据
|
||||
|
||||
## 1. Self Encapsulate Field(自封装字段)
|
||||
|
||||
为字段建立取值/设值函数,并用这些函数来访问字段。只有当子类想访问超类的一个字段,又想在子类中将对这个字段访问改为一个计算后的值,才使用这种方式,否则直接访问字段的方式简洁明了。
|
||||
|
||||
## 2. Replace Data Value with Object(以对象取代数据值)
|
||||
|
||||
在开发初期,往往会用简单的数据项表示简单的情况,但是随着开发的进行,一些简单数据项会具有一些特殊行为。比如一开始会把电话号码存成字符串,但是随后发现电话号码需要“格式化”、“抽取区号”之类的特殊行为。
|
||||
|
||||
## 3. Change Value to Reference(将值对象改成引用对象)
|
||||
|
||||
将彼此相等的实例替换为同一个对象。这就要用一个工厂来创建这种唯一对象,工厂类中需要保留一份已经创建对象的列表,当要创建一个对象时,先查找这份列表中是否已经存在该对象,如果存在,则返回列表中的这个对象;否则,新建一个对象,添加到列表中,并返回该对象。
|
||||
|
||||
## 4. Change Reference to value(将引用对象改为值对象)
|
||||
|
||||
以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。
|
||||
|
||||
需要为值对象实现 equals() 和 hashCode() 方法
|
||||
|
||||
## 5. Replace Array with Object(以对象取代数组)
|
||||
|
||||
有一个数组,其中的元素各自代表不同的东西。
|
||||
|
||||
以对象替换数组,对于数组中的每个元素,以一个字段来表示,这样方便操作,也更容易理解。
|
||||
|
||||
## 6. Duplicate Observed Data(赋值“被监视数据”)
|
||||
|
||||
一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。
|
||||
|
||||
将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。
|
||||
|
||||

|
||||
|
||||
## 7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
|
||||
|
||||
当两个类都需要对方的特性时,可以使用双向关联。
|
||||
|
||||
有两个类,分别为订单 Order 和客户 Customer,Order 引用了 Customer,Customer 也需要引用 Order 来查看其所有订单详情。
|
||||
|
||||
```java
|
||||
class Order{
|
||||
private Customer customer;
|
||||
public void setCustomer(Customer customer){
|
||||
if(this.customer != null)
|
||||
this.customer.removeOrder(this);
|
||||
this.customer = customer;
|
||||
this.customer.add(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
```java
|
||||
class Curstomer{
|
||||
private Set<Order> orders = new HashSet<>();
|
||||
public void removeOrder(Order order){
|
||||
orders.remove(order);
|
||||
}
|
||||
public void addOrder(Order order){
|
||||
orders.add(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意到,这里让 Curstomer 类来控制关联关系。有以下原则来决定哪个类来控制关联关系:如果某个对象是组成另一个对象的部件,那么由后者负责控制关联关系;如果是一对多关系,则由单一引用那一方来控制关联关系。
|
||||
|
||||
## 8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
|
||||
|
||||
和 Change Unidirectional Association to Bidirectiona 为反操作。
|
||||
|
||||
双向关联维护成本高,并且也不易于理解。大量的双向连接很容易造成“僵尸对象”:某个对象本身已经死亡了,却保留在系统中,因为它的引用还没有全部完全清除。
|
||||
|
||||
## 9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
|
||||
|
||||
创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。
|
||||
|
||||
## 10. Encapsulate Field(封装字段)
|
||||
|
||||
public 字段应当改为 private,并提供相应的访问函数。
|
||||
|
||||
## 11. Encapsulate Collection(封装集合)
|
||||
|
||||
函数返回集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户得以修改集合内容而集合拥有者却一无所知。
|
||||
|
||||
## 12. Replace Record with Data Class(以数据类取代记录)
|
||||
|
||||
## 13. Replace Type Code with Class(以类取代类型码)
|
||||
|
||||
类中有一个数值类型码,但它并不影响类的行为,就用一个新类替换该数值类型码。如果类型码出现在 switch 语句中,需要使用 Replace Conditional with Polymorphism 去掉 switch,首先必须运用 Replace Type Code with Subcalss 或 Replace Type Code with State/Strategy 去掉类型码。
|
||||
|
||||

|
||||
|
||||
## 14. Replace Type Code with Subcalsses(以子类取代类型码)
|
||||
|
||||
有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码。
|
||||
|
||||

|
||||
|
||||
## 15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)
|
||||
|
||||
有一个可变的类型码,它会影响类的行为,以状态对象取代类型码。
|
||||
|
||||
和 Replace Type Code with Subcalsses 的区别是 Replace Type Code with State/Strategy 的类型码是动态可变的,前者通过继承的方式来实现,后者通过组合的方式来实现。因为类型码可变,如果通过继承的方式,一旦一个对象的类型码改变,那么就要改变用新的对象来取代旧对象,而客户端难以改变新的对象。但是通过组合的方式,改变引用的状态类是很容易的。
|
||||
|
||||

|
||||
|
||||
## 16. Replace Subclass with Fields(以字段取代子类)
|
||||
|
||||
各个子类的唯一差别只在“返回常量数据”的函数上。
|
||||
|
||||

|
||||
|
||||
# 第九章 简化条件表达式
|
||||
|
||||
## 1. Decompose Conditional(分解条件表达式)
|
||||
|
||||
对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。
|
||||
|
||||
```java
|
||||
if(data.befor(SUMMER_START) || data.after(SUMMER_END))
|
||||
charge = quantity * winterRate + winterServiceCharge;
|
||||
else charge = quantity * summerRate;
|
||||
```
|
||||
|
||||
```java
|
||||
if(notSummer(date))
|
||||
charge = winterCharge(quantity);
|
||||
else charge = summerCharge(quantity);
|
||||
```
|
||||
|
||||
## 2. Consolidate Conditional Expression(合并条件表达式)
|
||||
|
||||
有一系列条件测试,都得到相同结果。
|
||||
|
||||
将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
|
||||
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
if (seniority < 2) return 0;
|
||||
if (monthsDisabled > 12 ) return 0;
|
||||
if (isPartTime) return 0;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
if (isNotEligibleForDisability()) return 0;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段)
|
||||
|
||||
在条件表达式的每个分支上有着相同的一段代码。
|
||||
|
||||
将这段重复代码搬移到条件表达式之外。
|
||||
|
||||
```java
|
||||
if (isSpecialDeal()){
|
||||
total = price * 0.95;
|
||||
send();
|
||||
} else {
|
||||
total = price * 0.98;
|
||||
send();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
if (isSpecialDeal()) {
|
||||
total = price * 0.95;
|
||||
} else {
|
||||
total = price * 0.98;
|
||||
}
|
||||
send();
|
||||
```
|
||||
|
||||
## 4. Remove Control Flag(移除控制标记)
|
||||
|
||||
在一系列布尔表达式中,某个变量带有“控制标记”的作用。
|
||||
|
||||
用 break语 句或 return 语句来取代控制标记。
|
||||
|
||||
## 5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)
|
||||
|
||||
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为“卫语句”(guard clauses)。
|
||||
|
||||
条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况,可以使用卫语句表现所有特殊情况。
|
||||
|
||||
```java
|
||||
double getPayAmount() {
|
||||
double result;
|
||||
if (isDead) result = deadAmount();
|
||||
else {
|
||||
if (isSeparated) result = separatedAmount();
|
||||
else {
|
||||
if (isRetired) result = retiredAmount();
|
||||
else result = normalPayAmount();
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
```
|
||||
|
||||
```java
|
||||
double getPayAmount() {
|
||||
if (isDead) return deadAmount();
|
||||
if (isSeparated) return separatedAmount();
|
||||
if (isRetired) return retiredAmount();
|
||||
return normalPayAmount();
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Replace Conditional with Polymorphism (以多态取代条件表达式)
|
||||
|
||||
将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。需要先使用 Replace Type Code with Subclass 或 Replace Type Code with State/Strategy 来建立继承结果。
|
||||
|
||||
```java
|
||||
double getSpeed() {
|
||||
switch (type) {
|
||||
case EUROPEAN:
|
||||
return getBaseSpeed();
|
||||
case AFRICAN:
|
||||
return getBaseSpeed()- getLoadFactor()* numberOfCoconuts;
|
||||
case NORWEGIAN_BLUE:
|
||||
return isNailed ? 0 : getBaseSpeed(voltage);
|
||||
}
|
||||
throw new RuntimeException("Should be unreachable");
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 7. Introduce Null Object(引入Null对象)
|
||||
|
||||
将 null 值替换为 null 对象。这样做的好处在于,不需要询问对象是否为空,直接调用就行。
|
||||
|
||||
```java
|
||||
if (customer == null) plan = BillingPlan.basic();
|
||||
else plan = customer.getPlan();
|
||||
```
|
||||
|
||||
## 8. Introduce Assertion(引入断言)
|
||||
|
||||
以断言明确表现某种假设。断言只能用于开发过程中,产品代码中不会有断言。
|
||||
|
||||
```java
|
||||
double getExpenseLimit() {
|
||||
// should have either expense limit or a primary project
|
||||
return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
double getExpenseLimit() {
|
||||
Assert.isTrue (expenseLimit != NULL_EXPENSE || primaryProject != null);
|
||||
return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
|
||||
}
|
||||
```
|
||||
|
||||
# 第十章 简化函数调用
|
||||
|
||||
## 1. Rename Method(函数改名)
|
||||
|
||||
使函数名能解释函数的用途。
|
||||
|
||||
## 2. Add Parameter(添加参数)
|
||||
|
||||
使函数不需要通过调用获得某个信息。
|
||||
|
||||
## 3. Remove Parameter(移除参数)
|
||||
|
||||
与 Add Parameter 相反,改用调用的方式来获得某个信息。
|
||||
|
||||
## 4. Separate Query from Modifier(将查询函数和修改函数分离)
|
||||
|
||||
某个函数即返回对象状态值,又修改对象状态。
|
||||
|
||||
应当建立两个不同的函数,其中一个负责查询,另一个负责修改。任何有返回值的函数,都不应该有看得到的副作用。
|
||||
|
||||
```java
|
||||
getTotalOutstandingAndSetReadyForSummaries();
|
||||
```
|
||||
|
||||
```java
|
||||
getTotalOutstanding();
|
||||
setReadyForSummaries();
|
||||
```
|
||||
|
||||
## 5. Parameterize Method(令函数携带参数)
|
||||
|
||||
若干函数做了类似的工作,但在函数本体中却包含了不同的值。
|
||||
|
||||
建立单一函数,以参数表达那些不同的值。
|
||||
|
||||
```java
|
||||
fivePercentRaise();
|
||||
tenPercentRaise();
|
||||
```
|
||||
```java
|
||||
raise(percentage);
|
||||
```
|
||||
|
||||
## 6. Replace Parameter with Explicit Methods(以明确函数取代参数)
|
||||
|
||||
有一个函数,完全取决于参数值而采取不同行为。
|
||||
|
||||
针对该参数的每一个可能值,建立一个独立函数。
|
||||
|
||||
```java
|
||||
void setValue(String name, int value){
|
||||
if (name.equals("height")){
|
||||
height = value;
|
||||
return;
|
||||
}
|
||||
if (name.equals("width")){
|
||||
width = value;
|
||||
return;
|
||||
}
|
||||
Assert.shouldNeverReachHere();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
void setHeight(int arg){
|
||||
height = arg;
|
||||
}
|
||||
void setWidth(int arg){
|
||||
width = arg;
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Preserve Whole Object(保持对象完整)
|
||||
|
||||
从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
|
||||
|
||||
改为传递整个对象。
|
||||
|
||||
```java
|
||||
int low = daysTempRange().getLow();
|
||||
int high = daysTempRange().getHigh();
|
||||
withinPlan = plan.withinRange(low,high);
|
||||
```
|
||||
|
||||
```java
|
||||
withinPlan = plan.withinRange(daysTempRange());
|
||||
```
|
||||
|
||||
## 8. Replace Parameter with Methods(以函数取代参数)
|
||||
|
||||
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
|
||||
|
||||
让参数接收者去除该项参数,而是直接调用前一个函数。
|
||||
|
||||
```java
|
||||
int basePrice = _quantity * _itemPrice;
|
||||
discountLevel = getDiscountLevel();
|
||||
double finalPrice = discountedPrice (basePrice, discountLevel);
|
||||
```
|
||||
|
||||
```java
|
||||
int basePrice = _quantity * _itemPrice;
|
||||
double finalPrice = discountedPrice (basePrice);
|
||||
```
|
||||
|
||||
## 9. Introduce Parameter Object(引入参数对象)
|
||||
|
||||
某些参数总是很自然地同时出现,这些参数就是 Data Clumps。
|
||||
|
||||
以一个对象取代这些参数。
|
||||
|
||||

|
||||
|
||||
## 10. Remove Setting Method(移除设值函数)
|
||||
|
||||
类中的某个字段应该在对象创建时被设值,然后就不再改变。
|
||||
|
||||
去掉该字段的所有设值函数,并将该字段设为 final。
|
||||
|
||||
## 11. Hide Method(隐藏函数)
|
||||
|
||||
有一个函数,从来没有被其他任何类用到。
|
||||
|
||||
将这个函数修改为 private。
|
||||
|
||||
## 12. Replace Constructor with Factory Method (以工厂函数取代构造函数)
|
||||
|
||||
希望在创建对象时不仅仅是做简单的建构动作。
|
||||
|
||||
将构造函数替换为工厂函数。
|
||||
|
||||
## 13. Encapsulate Downcast(封装向下转型)
|
||||
|
||||
某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。
|
||||
|
||||
将向下转型动作移到函数中。
|
||||
|
||||
```java
|
||||
Object lastReading(){
|
||||
return readings.lastElement();
|
||||
}
|
||||
```
|
||||
```java
|
||||
Reading lastReading(){
|
||||
return (Reading)readings.lastElement();
|
||||
}
|
||||
```
|
||||
|
||||
## 14. Replace Error Code with Exception (以异常取代错误码)
|
||||
|
||||
某个函数返回一个特定的代码,用以表示某种错误情况。
|
||||
|
||||
改用异常,异常将普通程序和错误处理分开,使代码更容易理解。
|
||||
|
||||
## 15. Replace Exception with Test(以测试取代异常)
|
||||
|
||||
面对一个调用者可以预先检查的条件,你抛出了一个异常。
|
||||
|
||||
修改调用者,使它在调用函数之前先做检查。
|
||||
|
||||
```java
|
||||
double getValueForPeriod(int periodNumber) {
|
||||
try {
|
||||
return values[periodNumber];
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
```java
|
||||
double getValueForPeriod(int periodNumber) {
|
||||
if (periodNumber >= values.length) return 0;
|
||||
return values[periodNumber];
|
||||
```
|
||||
|
||||
# 第十一章 处理概括关系
|
||||
|
||||
## 1. Pull Up Field(字段上移)
|
||||
|
||||
两个子类拥有相同的字段。
|
||||
|
||||
将该字段移至超类。
|
||||
|
||||
## 2. Pull Up Method(函数上移)
|
||||
|
||||
有些函数,在各个子类中产生完全相同的结果。
|
||||
|
||||
将该函数移至超类。
|
||||
|
||||
## 3. Pull Up Constructor Body(构造函数本体上移)
|
||||
|
||||
你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。
|
||||
|
||||
在超类中新建一个构造函数,并在子类构造函数中调用它。
|
||||
|
||||
```java
|
||||
class Manager extends Employee...
|
||||
|
||||
public Manager(String name, String id, int grade) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.grade = grade;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public Manager(String name, String id, int grade) {
|
||||
super(name, id);
|
||||
this.grade = grade;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Push Down Method(函数下移)
|
||||
|
||||
超类中的某个函数只与部分子类有关。
|
||||
|
||||
将这个函数移到相关的那些子类去。
|
||||
|
||||
## 5. Push Down Field(字段下移)
|
||||
|
||||
超类中的某个字段只被部分子类用到。
|
||||
|
||||
将这个字段移到需要它的那些子类去。
|
||||
|
||||
## 6. Extract Subclass(提炼子类)
|
||||
|
||||
类中的某些特性只被某些实例用到。
|
||||
|
||||
新建一个子类,将上面所说的那一部分特性移到子类中。
|
||||
|
||||
## 7. Extract Superclass(提炼超类)
|
||||
|
||||
两个类有相似特性。
|
||||
|
||||
为这两个类建立一个超类,将相同特性移至超类。
|
||||
|
||||
## 8. Extract Interface(提炼接口)
|
||||
|
||||
若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。
|
||||
|
||||
将相同的子集提炼到一个独立接口中。
|
||||
|
||||
## 9. Collapse Hierarchy(折叠继承体系)
|
||||
|
||||
超类和子类之间无太大区别。
|
||||
|
||||
将它们合为一体。
|
||||
|
||||
## 10. Form Template Method(塑造模板函数)
|
||||
|
||||
你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
|
||||
|
||||
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。(模板方法模式)
|
||||
|
||||
## 11. Replace Inheritance with Delegation (以委托取代继承)
|
||||
|
||||
某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。
|
||||
|
||||
在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系。
|
||||
|
||||
## 12. Replace Delegation with Inheritance (以继承取代委托)
|
||||
|
||||
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。
|
||||
|
||||
让委托类继承受托类。
|
@ -1,292 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# S.O.L.I.D
|
||||
|
||||
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。
|
||||
|
||||
|简写 |全拼 |中文翻译|
|
||||
| -- | -- | -- |
|
||||
|SRP| The Single Responsibility Principle |单一责任原则|
|
||||
|OCP| The Open Closed Principle | 开放封闭原则|
|
||||
|LSP| The Liskov Substitution Principle |里氏替换原则|
|
||||
|ISP| The Interface Segregation Principle |接口分离原则|
|
||||
|DIP| The Dependency Inversion Principle |依赖倒置原则|
|
||||
|
||||
|
||||
## 1. 单一责任原则
|
||||
|
||||
当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
|
||||
|
||||
## 2. 开放封闭原则
|
||||
|
||||
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
|
||||
|
||||
## 3. 里氏替换原则
|
||||
|
||||
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有 is-a 关系。
|
||||
|
||||
## 4. 接口分离原则
|
||||
|
||||
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
|
||||
|
||||
## 5. 依赖倒置原则
|
||||
|
||||
1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
|
||||
2. 抽象不应该依赖于细节,细节应该依赖于抽象
|
||||
|
||||
# 封装、继承、多态
|
||||
|
||||
封装、继承、多态是面向对象的三大特性。
|
||||
|
||||
## 1. 封装
|
||||
|
||||
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
|
||||
|
||||
封装有三大好处:
|
||||
|
||||
1. 良好的封装能够减少耦合。
|
||||
|
||||
2. 类内部的结构可以自由修改。
|
||||
|
||||
3. 可以对成员进行更精确的控制。
|
||||
|
||||
4. 隐藏信息,实现细节。
|
||||
|
||||
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
|
||||
|
||||
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改使用的数据类型时,也可以在不影响客户端代码的情况下进行。
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
private String name;
|
||||
private int gender;
|
||||
private int age;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getGender() {
|
||||
return gender == 0 ? "man" : "woman";
|
||||
}
|
||||
|
||||
public void work() {
|
||||
if(18 <= age && age <= 50) {
|
||||
System.out.println(name + " is working very hard!");
|
||||
} else {
|
||||
System.out.println(name + " can't work!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 继承
|
||||
|
||||
继承实现了 **is-a** 关系,例如 Cat 和 Animal 就是一种 is-a 关系,因此可以将 Cat 继承自 Animal,从而获得 Animal 非 private 的属性和方法。
|
||||
|
||||
Cat 可以当做 Animal 来使用,也就是可以使用 Animal 引用 Cat 对象,这种子类转换为父类称为 **向上转型**。
|
||||
|
||||
继承应该遵循里氏替换原则:当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有 is-a 关系。
|
||||
|
||||
```java
|
||||
Animal animal = new Cat();
|
||||
```
|
||||
|
||||
## 3. 多态
|
||||
|
||||
多态分为编译时多态和运行时多态。编译时多态主要指方法的重装,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
|
||||
|
||||
多态有三个条件:1. 继承;2. 覆盖父类方法;3. 向上转型。
|
||||
|
||||
下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
|
||||
|
||||
```java
|
||||
public class Instrument {
|
||||
public void play() {
|
||||
System.out.println("Instument is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Wind extends Instrument {
|
||||
public void play() {
|
||||
System.out.println("Wind is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Percussion extends Instrument {
|
||||
public void play() {
|
||||
System.out.println("Percussion is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Music {
|
||||
public static void main(String[] args){
|
||||
List<Instrument> instruments = new ArrayList<>();
|
||||
instruments.add(new Wind());
|
||||
instruments.add(new Percussion());
|
||||
for(Instrument instrument : instruments){
|
||||
instrument.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# UML
|
||||
|
||||
## 1. 类图
|
||||
|
||||
**1.1 继承相关**
|
||||
|
||||
继承有两种形式: 泛化(generalize)和实现(realize),表现为 is-a 关系。
|
||||
|
||||
① 泛化关系(generalization)
|
||||
|
||||
从具体类中继承
|
||||
|
||||

|
||||
|
||||
② 实现关系(realize)
|
||||
|
||||
从抽象类或者接口中继承
|
||||
|
||||

|
||||
|
||||
**1.2 整体和部分**
|
||||
|
||||
① 聚合关系(aggregation)
|
||||
|
||||
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成:
|
||||
|
||||

|
||||
|
||||
|
||||
② 组合关系(composition)
|
||||
|
||||
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
|
||||
|
||||

|
||||
|
||||
**1.3 相互联系**
|
||||
|
||||
① 关联关系(association)
|
||||
|
||||
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
|
||||
|
||||

|
||||
|
||||
② 依赖关系(dependency)
|
||||
|
||||
和关联关系不同的是, 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。
|
||||
|
||||

|
||||
|
||||
|
||||
## 2. 时序图
|
||||
|
||||
**2.1 定义**
|
||||
|
||||
时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。它的主要作用是通过对象间的交互来描述用例(注意是对象),从而寻找类的操作。
|
||||
|
||||
**2.2 赤壁之战时序图**
|
||||
|
||||
从虚线从上往下表示时间的推进。
|
||||
|
||||

|
||||
|
||||
可见,通过时序图可以知道每个类具有以下操作:
|
||||
|
||||
```java
|
||||
publc class 刘备 {
|
||||
public void 应战();
|
||||
}
|
||||
|
||||
publc class 孔明 {
|
||||
public void 拟定策略();
|
||||
public void 联合孙权();
|
||||
private void 借东风火攻();
|
||||
}
|
||||
|
||||
public class 关羽 {
|
||||
public void 防守荊州();
|
||||
}
|
||||
|
||||
public class 张飞 {
|
||||
public void 防守荆州前线();
|
||||
}
|
||||
|
||||
public class 孙权 {
|
||||
public void 领兵相助();
|
||||
}
|
||||
```
|
||||
|
||||
**2.3 活动图、时序图之间的关系**
|
||||
|
||||
活动图示从用户的角度来描述用例;
|
||||
|
||||
时序图是从计算机的角度(对象间的交互)描述用例。
|
||||
|
||||
**2.4 类图与时序图的关系**
|
||||
|
||||
类图描述系统的静态结构,时序图描述系统的动态行为。
|
||||
|
||||
**2.5 时序图的组成**
|
||||
|
||||
① 对象
|
||||
|
||||
有三种表现形式
|
||||
|
||||

|
||||
|
||||
在画图时,应该遵循以下原则:
|
||||
|
||||
1. 把交互频繁的对象尽可能地靠拢。
|
||||
|
||||
2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。
|
||||
|
||||
② 生命线
|
||||
|
||||
生命线从对象的创建开始到对象销毁时终止
|
||||
|
||||

|
||||
|
||||
③ 消息
|
||||
|
||||
对象之间的交互式通过发送消息来实现的。
|
||||
|
||||
消息有4种类型:
|
||||
|
||||
1\. 简单消息,不区分同步异步。
|
||||
|
||||

|
||||
|
||||
2\. 同步消息,发送消息之后需要暂停活动来等待回应。
|
||||
|
||||

|
||||
|
||||
3\. 异步消息,发送消息之后不需要等待。
|
||||
|
||||

|
||||
|
||||
4\. 返回消息,可选。
|
||||
|
||||
④ 激活
|
||||
|
||||
生命线上的方框表示激活状态,其它时间处于休眠状态。
|
||||
|
||||

|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Java 编程思想
|
||||
|
||||
- [面向对象设计的SOLID原则](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html)
|
||||
|
||||
- [看懂UML类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization)
|
||||
|
||||
- [UML系列——时序图(顺序图)sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html)
|
||||
|
||||
- [面向对象编程三大特性------封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015)
|
@ -1,403 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 解决思路
|
||||
|
||||
面向对象设计问题要求求职者设计出类和方法,以实现技术问题或描述真实生活中的对象。可以按照以下步骤来进行解决:
|
||||
|
||||
## 1. 处理不明确的地方
|
||||
|
||||
面向对象设计问题往往会故意放些烟幕弹,意在检验你是武断臆测,还是提出问题以厘清问题。毕竟,开发人员要是没弄清楚自己要开发什么,就直接挽起袖子开始编码,只会浪费公司的财力物力,还可能造成更严重的后果。
|
||||
|
||||
碰到面向对象设计问题时,你应该先问清楚,谁是使用者、他们将如何使用。对某些问题,你甚至还要问清楚“5W1H”,也就是 Who(谁)、What(什么)、Where(哪里)、When(何时)Why(为什么)、How(如何)。举个例子,假设面试官让你描述咖啡机的面向对象设计。这个问题看似简单明了,其实不然。这台咖啡机可能是一款工业型机器,设计用来放在大餐厅里,每小时要服务几百位顾客,还要能制作 10 种不同口味的咖啡。又或者,它可能是设计给老年人使用的简易咖啡机,只要能制作简单的黑咖啡就行。这些用例将大大影响你的设计。
|
||||
|
||||
## 2. 定义核心对象
|
||||
|
||||
比如,假设要为一家餐馆进行面向对象设计。那么,核心对象可能包括员工、服务员、厨师、餐桌、顾客、订单、菜。
|
||||
|
||||
## 3. 分析对象关系
|
||||
|
||||
可以利用 UML 类图来分析。其中,
|
||||
|
||||
- 服务员和厨师都继承自员工类;
|
||||
- 一个服务员要为多个餐桌的顾客提供服务;
|
||||
- 一个餐桌可以有很多顾客;
|
||||
- 一个餐桌可以有一个订单;
|
||||
- 一个订单有多个菜。
|
||||
- 一个厨师要处理多个订单;
|
||||
|
||||

|
||||
|
||||
## 4. 研究对象的动作
|
||||
|
||||
可以用时序图来找出对象的动作。
|
||||
|
||||
多个顾客坐到一个餐桌,服务员来到餐桌前提供服务,并等待顾客填写订单。订单填写完成后,交给厨师处理订单,并等待厨师处理完成,完成之后服务员将上菜。
|
||||
|
||||

|
||||
|
||||
# 示例
|
||||
|
||||
## 1. 聊天服务器
|
||||
|
||||
题目描述:请描述该如何设计一个聊天服务器。要求给出各种后台组件、类和方法的细节,并说明其中最难解决的问题会是什么。
|
||||
|
||||
设计聊天服务器是项大工程,绝非一次面试就能完成。毕竟,就算一整个团队,也要花费数月乃至好几年才能打造出一个聊天服务器。作为求职者,你的工作是专注解决该问题的某个方面,涉及范围要够广,又要够集中,这样才能在一轮面试中搞定。它不一定要与真实情况一模一样,但也应该忠实反映出实际的实现。
|
||||
|
||||
这里我们会把注意力放在用户管理和对话等核心功能:添加用户、创建对话、更新状态,等等。考虑到时间和空间有限,我们不会探讨这个问题的联网部分,也不描述数据是怎么真正推送到客户端的。
|
||||
|
||||
另外,我们假设“好友关系”是双向的,如果你是我的联系人之一,那就表示我也是你的联系人之一。我们的聊天系统将支持群组聊天和一对一(私密)聊天,但并不考虑语音聊天、视频聊天或文件传输。
|
||||
|
||||
### 1.1 需要支持的特定动作
|
||||
|
||||
这也有待你跟面试官探讨,下面列出几点想法。
|
||||
|
||||
- 显示在线和离线状态。
|
||||
- 添加请求(发送、接受、拒绝)。
|
||||
- 更新状态信息。
|
||||
- 发起私聊和群聊。
|
||||
- 在私聊和群聊中添加新信息。
|
||||
|
||||
这只是一部分列表,如果时间有富余,还可以多加一些动作。
|
||||
|
||||
### 1.2 核心组件
|
||||
|
||||
这个系统可能由一个数据库、一组客户端和一组服务器组成。我们的面向对象设计不会包含这些部分,不过可以讨论一下系统的整体概览。
|
||||
|
||||
数据库将用来存放更持久的数据,比如用户列表或聊天对话的备份。 SQL 数据库应该是不错的选择,或者,如果可扩展性要求更高,可以选用 BigTable 或其他类似的系统。
|
||||
|
||||
对于客户端和服务器之间的通信,使用 XML 应该也不错。尽管这种格式不是最紧凑的(你也应该向面试官指出这一点),它仍是很不错的选择,因为不管是计算机还是人类都容易辨识。使用 XML 可以让程序调试起来更轻松,这一点非常重要。
|
||||
|
||||
服务器由一组机器组成,数据会分散到各台机器上,这样一来,我们可能就必须从一台机器跳到另一台机器。如果可能的话,我们会尽量在所有机器上复制部分数据,以减少查询操作的次数。在此,设计上有个重要的限制条件,就是必须防止出现单点故障。例如,如果一台机器控制所有用户的登录,那么,只要这一台机器断网,就会造成数以百万计的用户无法登录。
|
||||
|
||||
### 1.3 关键对象和方法
|
||||
|
||||
代码参考:[Github](https://github.com/careercup/ctci/tree/master/java/Chapter%208/Question8_7)
|
||||
|
||||
系统的关键对象包括用户、对话和状态消息等,我们已经实现了 UserManagement 类。要是更关注这个问题的联网方面或其他组件,我们就可能转而深入探究那些对象。
|
||||
|
||||
```java
|
||||
public class UserManager {
|
||||
private static UserManager instance;
|
||||
private HashMap<Integer, User> usersById = new HashMap<Integer, User>();
|
||||
private HashMap<String, User> usersByAccountName = new HashMap<String, User>();
|
||||
private HashMap<Integer, User> onlineUsers = new HashMap<Integer, User>();
|
||||
|
||||
public static UserManager getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new UserManager();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void addUser(User fromUser, String toAccountName) {
|
||||
User toUser = usersByAccountName.get(toAccountName);
|
||||
AddRequest req = new AddRequest(fromUser, toUser, new Date());
|
||||
toUser.receivedAddRequest(req);
|
||||
fromUser.sentAddRequest(req);
|
||||
}
|
||||
|
||||
public void approveAddRequest(AddRequest req) {
|
||||
req.status = RequestStatus.Accepted;
|
||||
User from = req.getFromUser();
|
||||
User to = req.getToUser();
|
||||
from.addContact(to);
|
||||
to.addContact(from);
|
||||
}
|
||||
|
||||
public void rejectAddRequest(AddRequest req) {
|
||||
req.status = RequestStatus.Rejected;
|
||||
User from = req.getFromUser();
|
||||
User to = req.getToUser();
|
||||
from.removeAddRequest(req);
|
||||
to.removeAddRequest(req);
|
||||
}
|
||||
|
||||
public void userSignedOn(String accountName) {
|
||||
User user = usersByAccountName.get(accountName);
|
||||
if (user != null) {
|
||||
user.setStatus(new UserStatus(UserStatusType.Available, ""));
|
||||
onlineUsers.put(user.getId(), user);
|
||||
}
|
||||
}
|
||||
|
||||
public void userSignedOff(String accountName) {
|
||||
User user = usersByAccountName.get(accountName);
|
||||
if (user != null) {
|
||||
user.setStatus(new UserStatus(UserStatusType.Offline, ""));
|
||||
onlineUsers.remove(user.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 User 类中, receivedAddRequest 方法会通知用户 B(User B),用户 A(User A)请求加他 为 好 友 。 用 户 B 会 接 受 或 拒 绝 该 请 求 ( 通 过 UserManager.approveAddRequest 或 rejectAddRequest), UserManager 则负责将用户互相添加到对方的通讯录中。
|
||||
|
||||
当 UserManager 要将 AddRequest 加入用户 A 的请求列表时,会调用 User 类的 sentAddRequest 方法。综上,整个流程如下。
|
||||
|
||||
1. 用户 A 点击客户端软件上的“添加用户”,发送给服务器。
|
||||
2. 用户 A 调用 requestAddUser(User B)。
|
||||
3. 步骤 2 的方法会调用 UserManager.addUser。
|
||||
4. UserManager 会调用 User A.sentAddRequest 和 User B.receivedAddRequest。
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private int id;
|
||||
private UserStatus status = null;
|
||||
private HashMap<Integer, PrivateChat> privateChats = new HashMap<Integer, PrivateChat>();
|
||||
private ArrayList<GroupChat> groupChats = new ArrayList<GroupChat>();
|
||||
private HashMap<Integer, AddRequest> receivedAddRequests = new HashMap<Integer, AddRequest>();
|
||||
private HashMap<Integer, AddRequest> sentAddRequests = new HashMap<Integer, AddRequest>();
|
||||
|
||||
private HashMap<Integer, User> contacts = new HashMap<Integer, User>();
|
||||
private String accountName;
|
||||
private String fullName;
|
||||
|
||||
public User(int id, String accountName, String fullName) {
|
||||
this.accountName = accountName;
|
||||
this.fullName = fullName;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public boolean sendMessageToUser(User toUser, String content) {
|
||||
PrivateChat chat = privateChats.get(toUser.getId());
|
||||
if (chat == null) {
|
||||
chat = new PrivateChat(this, toUser);
|
||||
privateChats.put(toUser.getId(), chat);
|
||||
}
|
||||
Message message = new Message(content, new Date());
|
||||
return chat.addMessage(message);
|
||||
}
|
||||
|
||||
public boolean sendMessageToGroupChat(int groupId, String content) {
|
||||
GroupChat chat = groupChats.get(groupId);
|
||||
if (chat != null) {
|
||||
Message message = new Message(content, new Date());
|
||||
return chat.addMessage(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setStatus(UserStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public UserStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean addContact(User user) {
|
||||
if (contacts.containsKey(user.getId())) {
|
||||
return false;
|
||||
} else {
|
||||
contacts.put(user.getId(), user);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void receivedAddRequest(AddRequest req) {
|
||||
int senderId = req.getFromUser().getId();
|
||||
if (!receivedAddRequests.containsKey(senderId)) {
|
||||
receivedAddRequests.put(senderId, req);
|
||||
}
|
||||
}
|
||||
|
||||
public void sentAddRequest(AddRequest req) {
|
||||
int receiverId = req.getFromUser().getId();
|
||||
if (!sentAddRequests.containsKey(receiverId)) {
|
||||
sentAddRequests.put(receiverId, req);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAddRequest(AddRequest req) {
|
||||
if (req.getToUser() == this) {
|
||||
receivedAddRequests.remove(req);
|
||||
} else if (req.getFromUser() == this) {
|
||||
sentAddRequests.remove(req);
|
||||
}
|
||||
}
|
||||
|
||||
public void requestAddUser(String accountName) {
|
||||
UserManager.getInstance().addUser(this, accountName);
|
||||
}
|
||||
|
||||
public void addConversation(PrivateChat conversation) {
|
||||
User otherUser = conversation.getOtherParticipant(this);
|
||||
privateChats.put(otherUser.getId(), conversation);
|
||||
}
|
||||
|
||||
public void addConversation(GroupChat conversation) {
|
||||
groupChats.add(conversation);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Conversation 类实现为一个抽象类,因为所有 Conversation 不是 GroupChat 就是 PrivateChat,同时每个类各有自己的功能。
|
||||
|
||||
```java
|
||||
public abstract class Conversation {
|
||||
protected ArrayList<User> participants = new ArrayList<User>();
|
||||
protected int id;
|
||||
protected ArrayList<Message> messages = new ArrayList<Message>();
|
||||
|
||||
public ArrayList<Message> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public boolean addMessage(Message m) {
|
||||
messages.add(m);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class GroupChat extends Conversation {
|
||||
public void removeParticipant(User user) {
|
||||
participants.remove(user);
|
||||
}
|
||||
|
||||
public void addParticipant(User user) {
|
||||
participants.add(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class PrivateChat extends Conversation {
|
||||
public PrivateChat(User user1, User user2) {
|
||||
participants.add(user1);
|
||||
participants.add(user2);
|
||||
}
|
||||
|
||||
public User getOtherParticipant(User primary) {
|
||||
if (participants.get(0) == primary) {
|
||||
return participants.get(1);
|
||||
} else if (participants.get(1) == primary) {
|
||||
return participants.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class Message {
|
||||
private String content;
|
||||
private Date date;
|
||||
public Message(String content, Date date) {
|
||||
this.content = content;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
AddRequest 和 UserStatus 两个类比较简单,功能不多,主要用来将数据聚合在一起,方便其他类使用。
|
||||
|
||||
```java
|
||||
public class AddRequest {
|
||||
private User fromUser;
|
||||
private User toUser;
|
||||
private Date date;
|
||||
RequestStatus status;
|
||||
|
||||
public AddRequest(User from, User to, Date date) {
|
||||
fromUser = from;
|
||||
toUser = to;
|
||||
this.date = date;
|
||||
status = RequestStatus.Unread;
|
||||
}
|
||||
|
||||
public RequestStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public User getFromUser() {
|
||||
return fromUser;
|
||||
}
|
||||
|
||||
public User getToUser() {
|
||||
return toUser;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class UserStatus {
|
||||
private String message;
|
||||
private UserStatusType type;
|
||||
public UserStatus(UserStatusType type, String message) {
|
||||
this.type = type;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public UserStatusType getStatusType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public enum UserStatusType {
|
||||
Offline, Away, Idle, Available, Busy
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public enum RequestStatus {
|
||||
Unread, Read, Accepted, Rejected
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 最难解决或最有意思的问题
|
||||
|
||||
**问题 1:如何确定某人在线**
|
||||
|
||||
虽然希望用户在退出时通知我们,但即便如此也无法确切知道状态。例如,用户的网络连接可能断开了。为了确定用户何时退出,或许可以试着定期询问客户端,以确保它仍然在线。
|
||||
|
||||
**问题 2:如何处理冲突的信息**
|
||||
|
||||
部分信息存储在计算机内存中,部分则存储在数据库里。如果两者不同步有冲突,那会出什么问题?哪一部分是“正确的”?
|
||||
|
||||
**问题 3:如何才能让服务器在任何负载下都能应付自如**
|
||||
|
||||
前面我们设计聊天服务器时并没怎么考虑可扩展性,但在实际场景中必须予以关注。我们需要将数据分散到多台服务器上,而这又要求我们更关注数据的不同步。
|
||||
|
||||
**问题 4:如何预防拒绝服务攻击**
|
||||
|
||||
客户端可以向我们推送数据——若它们试图向服务器发起拒绝服务(DOS)攻击,怎么办?该如何预防?
|
||||
|
||||
# 参考资料
|
||||
|
||||
- GAYLELEAKMANNMCDOWELL. 程序员面试金典 [M]. 人民邮电出版社 , 2013.
|
@ -1,128 +0,0 @@
|
||||
[TOC]
|
||||
|
||||
# 译者序
|
||||
|
||||
作者:美国互联网创业之父
|
||||
|
||||
最开始 hack 指的是难题的解决办法,那些最能干的人被成为 hacker。黑客包含以下三个特点:好玩、高智商、探索精神。黑客的核心价值观:分享、开放、民主、计算机的自由使用、进步。因此黑客指的是那些专家级程序员。
|
||||
|
||||
由于黑客常常入侵系统,因此被与那些破坏计算机系统的人联系起来。然而破坏计算机系统的人称为骇客,真正的黑客不这么做,反而是让世界变得更好。
|
||||
|
||||
# 为什么书呆子不受欢迎
|
||||
|
||||
作者认为,“书呆子”和“高智商”正相关,而“书呆子”和“受欢迎”负相关。
|
||||
|
||||
提出问题:既然书呆子智商高,那么为什么不想出受欢迎的方法?因为要想受欢迎,需要投入很大的时间精力去塑造个人魅力。而书呆子没有那么多的时间和精力,相比于受欢迎,他们更在乎其它更有趣的事,比如设计奇妙的火箭等。
|
||||
|
||||
欺负书呆子的原因:
|
||||
|
||||
- 青少年的孩子更加残忍,书呆子不受欢迎意味着被歧视和受欺负。
|
||||
> 11 岁以前,小孩的生活由家长主导,其他孩子的影响有限。 孩子们不是不关心小学里其他同学的想法,但是后者不具有决定性影响。小学毕业以后,这种情形开始发生变化。到了 11 岁左右,孩子们逐渐把家庭生活当作寻常事了。 他们在同伴中开辟了一个新的世界,并认为那个世界才是重要的,比家里的世界更重要。实际上,如果他们在家里与父母发生冲突, 反而能在那个新的世界中挣得面子,而他们也确实更在乎那个世界。但是,问题在于,孩子们自己创造出来的世界是一个非常原始的世界。青少年在心理上还没有摆脱儿童状态, 许多人都会残忍地对待他人。他们折磨书呆子的原因就像拔掉一条蜘蛛腿一样,觉得很好玩。在一个人产生良知之前,折磨就是一种娱乐。
|
||||
|
||||
- 为了凸显自己。
|
||||
> 在任何社会等级制度中,那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到,正是因为这个原因,在美国社会中底层白人是对待黑人最残酷的群体。最受欢迎的孩子并不欺负书呆子,他们不需要靠踩在书呆子身上来垫高自己。大部分的欺负来自处于下一等级的学生,那些焦虑的中间层。
|
||||
|
||||
- 为了和受欢迎的人结成同盟。
|
||||
> 没有什么比一个共同的敌人更能使得人们团结起来了。这就好比一个政客,他想让选民忘记糟糕的国内局势,方法就是为国家找出一个敌人,哪怕敌人并不真的存在,他也可以创造一个出来。一群人在一起,挑出一个书呆子,居高临下地欺负他,就会把彼此联系起来。一起攻击一个外人,所有人因此就都成了自己人。这就是为什么最恶劣的以强凌弱的事件都与团体有关的原因。随便找一个书呆子,他都会告诉你,一群人的虐待比一个人的虐待残酷得多。
|
||||
|
||||
学校老师的只是把教书当成工作。
|
||||
> 公立学校的老师很像监狱的狱卒。看管监狱的人主要关心的是犯人都待在自己应该待的位置。然后,让犯人有东西吃,尽可能不要发生斗殴和伤害事件,这就可以了。
|
||||
|
||||
到社会之后,书呆子会被友好对待,因为他们做的事能够产生更多的影响,而社会正需要这些影响。
|
||||
|
||||
成年人把孩子在学校受苦受难归结为青春期的激素影响,但是实际上这是学校这种体制的问题,学校只是把孩子圈养起来,不让他们到处乱跑,然而学校内部存在着很多残忍。
|
||||
|
||||
过去的孩子更早投入社会,也更能够真正学会本领。
|
||||
> 过去的社会中,青少年扮演着一个更积极的角色。工业化时代到来前,青少年都是某种形式的学徒,不是在某个作坊,就是在某个农庄,甚至在某艘军舰上。他们不会被扔到一旁,创造自己的小社会。他们是成年人社会的低级成员。以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。
|
||||
|
||||
学校的使命是教书育人,而并没有外界的压力去监督他们这么做,所以老师和学生双方往往都是敷衍了事。
|
||||
|
||||
没有外在的对手,孩子们就互相把对方当作对手。
|
||||
|
||||
成年人贬低了很多美好的东西,比如“人格”。
|
||||
> 许多书呆子可能都与我一样,直到高中毕业多年后,才去读中学里的指定读物。但是,我错过的绝不仅仅只是几本书而已。我对许多美好的字眼都嗤之以鼻,比如“人格”、“正直”,因为成年人贬低了这些词。在他们嘴里,这些词似乎都是同一个意思——“听话”。一些孩子因为具备所谓的“人格”和“正直”,而受到夸奖,可是他们不是呆得像一头大笨牛,就是轻浮得像一个不动脑筋的吹牛者。如果“人格”和“正直”就是这种样子,我宁愿不要它们。
|
||||
|
||||
# 黑客与画家
|
||||
|
||||
黑客和画家一样,都是艺术家,都在进行创作。
|
||||
|
||||
黑客比较喜欢脚本语言那种可以动态扩展,并且可以像铅笔画画一样修修改改。
|
||||
> 编程语言首要的特性应该是允许动态扩展(malleable)。编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序。它应该是一支铅笔,而不是一支钢笔。如果大家都像学校教的那样编程,那么静态类型(static typing)是一个不错的概念。但是,我认识的黑客,没有一个人喜欢用静态类型语言编程。我们需要的是一种可以随意涂抹、擦擦改改的语言,我们不想正襟危坐,把一个盛满各种变量类型的茶杯,小心翼翼放在自己的膝盖上,为了与一丝不苟的编译器大婶交谈,努力地挑选词语,确保变量类型匹配,好让自己显得礼貌又周到。
|
||||
|
||||
编码并不是科学研究,因此数学家不一定写一手好代码。
|
||||
> 创作者不同于科学家,明白这一点有很多好处。除了不用为静态类型烦恼以外,还可以免去另一个折磨科学家的难题,那就是“对数学的妒忌”。科学界的每一个人,暗地里都相信数学家比自己聪明。我觉得,数学家自己也相信这一点。最后的结果就是科学家往往会把自己的工作尽可能弄得看上去像数学。对于物理学这样的领域,这可能不会有太大不良影响。但是,你越往自然科学的方向发展,它就越成为一个严重的问题。
|
||||
|
||||
黑客为了能做自己喜欢做的事并且可以养活自己,往往白天工作晚上做自己的事。
|
||||
|
||||
热爱编程的人不可避免的会开发自己的项目。
|
||||
> 我们面试程序员的时候,主要关注的事情就是业余时间他们写了什么软件。因为如果你不爱一件事,你不可能把它做得真正优秀,要是你很热爱编程,你就不可避免地会开发你自己的项目。
|
||||
|
||||
黑客不是机械工作的技术工人,而是一位创作者。
|
||||
> 如果黑客只是一个负责实现领导意志的技术工人,职责就是根据规格说明书写出代码,那么他其实与一个挖水沟的工人是一样的,从这头挖到那头,仅此而已。但是,如果黑客是一个创作者,他从事的就不是机械性的工作,他必须具备灵感。
|
||||
|
||||
# 不能说的话
|
||||
|
||||
流行的道德观念虽然在后世看来会是荒诞的,但是如果在现今去违背这些道德观念,说一些不合适的言论,会让自己处于麻烦之中。
|
||||
|
||||
自己会深深赞同自己的观点,并且确信会得到别人的认同,很可能是,这些观点是别人给自己灌输的,别人说什么,自己就相信什么。
|
||||
|
||||
不能说的话往往是正确的话,因为具有争议性,会让某些人暴跳如雷。
|
||||
|
||||
那些不一定正确,极富争议性的言论被成为“异端邪说“,人们常常会给它们加上标签,目的是为了封杀它们。
|
||||
> 亵渎神明、冒犯圣灵、异端都是西方历史上常见的标签,当代的标签则是有伤风化、不得体、破坏国家利益等。
|
||||
|
||||
禁忌的推崇者是那些处于中等阶层的人,他们不像高等阶层那样有足够自信保护自己的利益,又不同于低等阶层,他们有实力去推行。
|
||||
|
||||
流行观点的第一批追逐者是为了让自己与众不同,而第二批则是怕自己与众不同。
|
||||
|
||||
科学家往往需要打破流行观点,从而取得突破。
|
||||
|
||||
如果因为说了某些话而让干扰了自己的生活,最好还是别说。
|
||||
> 这时你要明白,自由思考比畅所欲言更重要。如果你感到一定要跟那些人辩个明白,绝不咽下这口气,一定要把话说清楚,结果很可能是从此你再也无法自由理性地思考了。我认为这样做不可取,更好的方法是在思想和言论之间划一条明确的界线。在心里无所不想,但是不一定要说出来。我就鼓励自己在心里默默思考那些最无法无天的想法。你的思想是一个地下组织,绝不要把那里发生的事情一股脑说给外人听。“格斗俱乐部”的第一条规则,就是不要提到格斗俱乐部。
|
||||
|
||||
当狂热分子质问你的观点时,最好是说自己还没想好。
|
||||
|
||||
人们会误认为自己思想开放,但是他们心里早就有一根界限,认为什么是正确的什么是错误的。
|
||||
|
||||
更深入的认识自己。
|
||||
> 儿童精疲力竭时,可能会大发脾气,因为他不知道为了什么;成年人则会了解是个人的身体状况问题,与外界无关,说一句“没关系,我只是累了”。
|
||||
|
||||
# 另一条路
|
||||
|
||||
互联网软件的优点
|
||||
|
||||
1. 不需要用户来管理系统,因此不需要专门的计算机知识,使用浏览器即可使用;
|
||||
2. 需要的硬件资源很少,只需要能运行浏览器并且联网的设备就行;
|
||||
3. 更方便,随时随地就可以使用;
|
||||
4. 不用担心数据丢失,因为数据保存在云端;
|
||||
5. 易于对软件进行升级,而用户可以在完全不知情的情况下继续使用;
|
||||
6. 更容易的 bug 发现,更快的 bug 修改;
|
||||
7. 有专门的技术人员管理服务器,因为更加安全;
|
||||
|
||||
|
||||
bug 越快发现越好,而互联网软件因为发布周期短,用户反馈快,因此 bug 的发现也更快。
|
||||
|
||||
高级用户很乐意帮助寻找 bug。
|
||||
> 因为软件发布以后,大多数 bug 都是罕见情况下才会发生的个案,受到影响的用户往往都是高级使用者,他们喜欢试验那些不常用的、难度大的操作。高级使用者对 bug 的容忍度比较高,尤其如果这些 bug 是在开发新功能的过程中引入的,而这些新功能又正是他们所需要的,他们就更能理解了。事实上,因为 bug 不多,你只有经过一些复杂的过程以后才会遇到它们,所以高级使用者往往因为发现了 bug 感到很得意。他们打电话给客服时,多半是一副胜利者的口吻,而不是怒气冲冲的样子,好像他们击败我们得分了一样。
|
||||
|
||||
好的技术支持是:客服人员和技术人员离得很近,并且当场修复 bug。
|
||||
|
||||
一个好的想法在实现过程中能引起更多想法,因此尽早去实现它比将它束之高阁能够带来更多好处。
|
||||
|
||||
不会购买软件的用户使用了盗版软件能够让软件更具有市场影响力。
|
||||
> 一定数量的盗版对软件公司是有好处的。不管你的软件定价多少,有些用户永远都不会购买。如果这样的用户使用盗版,你并没有任何损失。事实上,你反而赚到了,因为你的软件现在多了一个用户,市场影响力就更大了一些,而这个用户可能毕业以后就会出钱购买你的软件。
|
||||
|
||||
# 设计与研究
|
||||
|
||||
设计追求的是好,而研究追求的是新。
|
||||
|
||||
用户要求和用户需求有区别,用户可能并不了解自己要什么,所以用户要求的内容往往不是用户真正需求的东西。
|
||||
|
||||
艺术设计与人为本,而科学研究追求简洁正确。比如数学家不会为了让读者更容易懂而采用一种更麻烦的证明,而是会选择最直接简洁的证明。编程语言也是与人为本的,因此编程语言也是通过设计产生的。
|
||||
|
||||
大理石做出的东西漂亮,但是制作过程却很难,无法不断的进行雕琢。编程语言也需要这种雕琢过程,如果一种编程语言像大理石一样,只是结果好看,那么它就不是一种好的编程语言。
|
||||
|
||||
设计软件应当尽快拿出原型来。就像画画一样,是先用几根线画出一个大致准确的轮廓,然后再逐步加工。
|
||||
> 还有另一种软件设计思想,也许可以被称为“圣母玛丽亚”模式。它不要求尽快拿出原型,然后再逐步优化,它的观点是你应该等到完整的成品出来以后再一下子隆重地推向市场,就像圣母玛丽亚降临一样,哪怕整个过程漫长得像橄榄球运动员长途奔袭也没有关系。在互联网泡沫时期,无数创业公司因为相信了这种模式而自毁前程。
|
||||
|
||||
如果觉得做某件事很乏味,那么做出来的东西就会很乏味。快速的原型能够让程序员有更高的士气,从而让他们更有兴趣去实现。也可以理解为快速反馈很重要。
|
File diff suppressed because it is too large
Load Diff
@ -1,218 +0,0 @@
|
||||
Keven King
|
||||
编程人生
|
||||
随笔 - 91, 文章 - 0, 评论 - 11, 引用 - 0
|
||||
MySQL 经典面试题
|
||||
MySQL 面试 1 存储过程
|
||||
|
||||
什么是存储过程
|
||||
存储过程是一些编译好的SQL语句
|
||||
因为系统在调用SQL的时候比较浪费时间,所以之前先将一些基本的额SQL语句代码进行编译(对单表或多表的增删改查),然后再给代码取一个名字,在需要这个功能时去调用它就可以了。
|
||||
优缺点
|
||||
存储工程是编译后的代码 效率高
|
||||
存储过程代替SQL语句,降低网络通信
|
||||
在一定的程度确保数据安全
|
||||
2 索引
|
||||
|
||||
索引是什么
|
||||
索引是对数据库中一或多个列值的排序,帮助数据库高效获取数据的数据结构
|
||||
假如我们用类比的方法,数据库中的索引就相当于书籍中的目录一样,当我们想找到书中的摸个知识点,我们可以直接去目录中找而不是在书中每页的找,但是这也抛出了索引的一个缺点,在对数据库修改的时候要修改索引到导致时间变多。
|
||||
几个基本的索引类型 普通索引 唯一索引 主键索引 全文索引
|
||||
索引优点
|
||||
加快检索速度
|
||||
唯一索引确保每行数据的唯一性
|
||||
在使用索引的过程可以优化隐藏器,提高系统性能
|
||||
|
||||
|
||||
索引缺点
|
||||
插入删除 修改 维护速度下降
|
||||
占用物理和数据空间
|
||||
3 事务
|
||||
|
||||
事务的作用
|
||||
事务(Transaction)是并发控制的基本单位。事务就是一系列的操作,这些操作要么都执行,要么都不执行。
|
||||
事务具有以下4个基本特征
|
||||
Atomic(原子性) 事务中的一系列的操作要么都完成,要么全部失败
|
||||
Consistency(一致性) 一个成功的事务应该讲数据写入的到数据库,否则就要回滚到最初的状态
|
||||
Isolation(隔离性) 并发访问和修改的duli
|
||||
Durability(持久性) 事务结束应该讲事务的处理结构存储起来
|
||||
事务的语句
|
||||
开始事物:BEGIN TRANSACTION
|
||||
提交事物:COMMIT TRANSACTION
|
||||
回滚事务:ROLLBACK TRANSACTION
|
||||
4 数据库中的乐观锁和悲观锁
|
||||
|
||||
根据不同类型可以对数据设置不同的锁权限
|
||||
** 乐观 悲观 锁 主要是作用在并发访问控制**
|
||||
悲观锁 假定会发生并发冲突,屏蔽任何违反数据完整的操作
|
||||
乐观锁 假定不会发生冲突,只有在提交操作时检查是否违反数据的完整性
|
||||
5 drop, delete truncate 的区别
|
||||
|
||||
三者都是 删除 的意思,但是三者个有些区别
|
||||
delete和truncate只删除表的数据不删除表的结构
|
||||
速度 drop > truncate > delete
|
||||
想删除部分数据时, delete 删除时要带上where语句
|
||||
保留表而想删除所有的数据时用 truncate
|
||||
6 超键 候选键 主键 外键 区别
|
||||
|
||||
超键 在关系中能唯一标识元组的属性集称为关系模式的超键 ,一个或多个属性组合在一起作为超键。
|
||||
候选键 最下超键,没有冗余元素的超键
|
||||
主键 数据库中表中唯一和完整标识的数据列或属性集合。
|
||||
外键 在一个表中存在另外一个表的主键叫做外键 7视图
|
||||
|
||||
定义 视图是一种虚拟表,可以对视图进行增删查改 。可以将一个表多个表组合成一个视图。对视图的修改不影响基本表。 8 数据库三大范式介绍
|
||||
1NF 字段是最小单元,不可再分
|
||||
2NF 满足1NF 表中字段必须完全依赖全部主键而并非部分主键
|
||||
|
||||
|
||||
3NF 满足2NF,非主键外的所有字段必须互不依赖
|
||||
4.数据库三范式
|
||||
第一范式 字段具有原子性,不可再分
|
||||
第二范式 表中的每列都和主键相关
|
||||
第三范式 每列都和主键列直接相关,而不是间接相关
|
||||
|
||||
好文要顶 关注我 收藏该文
|
||||
|
||||
王守昌
|
||||
关注 - 0
|
||||
粉丝 - 9
|
||||
+加关注
|
||||
0
|
||||
0
|
||||
« 上一篇: MongoDB分片原理篇
|
||||
» 下一篇: 类图关系以及类图之间箭头表示
|
||||
|
||||
|
||||
posted on 2017-06-01 19:32 王守昌 阅读( 3298 ) 评论( 0 ) 编辑 收藏
|
||||
|
||||
刷新评论 刷新页面 返回顶部
|
||||
注册用户登录后才能发表评论,请 登录 或 注册 , 访问 网站首页。
|
||||
【推荐】50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
|
||||
【推荐】Vue.js 2.x 快速入门,大量高效实战示例
|
||||
【活动】腾讯云 学生专属优惠套餐 多规格选择
|
||||
【活动】释放技术的想象-解码腾讯云软件架构与应用
|
||||
|
||||
|
||||
最新IT新闻 :
|
||||
· 京东要用机器人和无人机对抗阿里巴巴 刘强东到底胜算几何?
|
||||
· TF Lite只是故事的一部分,谷歌还一并介绍了新的模型压缩方法
|
||||
· “大灰狼”远控开发者被曝猝死
|
||||
· UC浏览器遭Google Play下架,被指存在隐私问题
|
||||
· 董明珠:不反对在国外买技术但真正创造者要自主研发
|
||||
» 更多新闻...
|
||||
|
||||
最新知识库文章 :
|
||||
|
||||
· 关于编程,你的练习是不是有效的?
|
||||
· 改善程序员生活质量的 3+10 习惯
|
||||
· NASA的10条代码编写原则
|
||||
· 为什么你参加了那么多培训,却依然表现平平?
|
||||
· 写给初学前端工程师的一封信
|
||||
» 更多知识库文章...
|
||||
导航
|
||||
博客园
|
||||
首页
|
||||
新随笔
|
||||
联系
|
||||
订阅
|
||||
管理
|
||||
|
||||
< 2017年11月 >
|
||||
|
||||
日 一 二 三 四 五 六
|
||||
29 30 31 1 2 3 4
|
||||
5 6 7 8 9 10 11
|
||||
12 13 14 15 16 17 18
|
||||
19 20 21 22 23 24 25
|
||||
26 27 28 29 30 1 2
|
||||
3 4 5 6 7 8 9
|
||||
公告
|
||||
昵称: 王守昌
|
||||
园龄: 2年5个月
|
||||
粉丝: 9
|
||||
关注: 0
|
||||
+加关注
|
||||
搜索
|
||||
|
||||
|
||||
常用链接
|
||||
我的随笔
|
||||
我的评论
|
||||
我的参与
|
||||
最新评论
|
||||
我的标签
|
||||
|
||||
随笔分类
|
||||
git 学习与记录(1)
|
||||
Hadoop 安装(1)
|
||||
HTML和CSS(5)
|
||||
HTTP权威指南读书笔记(1)
|
||||
Java(3)
|
||||
java Web 分类 (9)
|
||||
Java 线程池(2)
|
||||
JavaScript 学习笔记(5)
|
||||
Java与设计模式(2)
|
||||
Linux (17)
|
||||
Maven 笔记(11)
|
||||
mongodb 随笔(17)
|
||||
python 入门教程
|
||||
rabbitmq 安装使用记录
|
||||
redis 学习随笔(3)
|
||||
thinking in java 笔记(1)
|
||||
数据结构与算法(1)
|
||||
随笔档案
|
||||
2017年10月 (4)
|
||||
2017年9月 (10)
|
||||
2017年8月 (5)
|
||||
2017年6月 (1)
|
||||
2017年5月 (15)
|
||||
2017年4月 (19)
|
||||
2017年3月 (8)
|
||||
2016年12月 (29)
|
||||
|
||||
最新评论
|
||||
|
||||
1. Re:如何高效学习读书笔记
|
||||
@王守昌谢谢。...
|
||||
--北落师门α
|
||||
2. Re:如何高效学习读书笔记
|
||||
@北落师门α用markdown编辑器上编辑,然后直接复制到博客园的编辑框中...
|
||||
--王守昌
|
||||
3. Re:如何高效学习读书笔记
|
||||
@退后一步是人生谢谢...
|
||||
--王守昌
|
||||
4. Re:如何高效学习读书笔记
|
||||
问下,笔记用什么记录的。
|
||||
想请教下怎么整理的这么整齐。什么软件上先写好,再粘到园子里的吧。
|
||||
--北落师门α
|
||||
5. Re:如何高效学习读书笔记
|
||||
楼主好总结!!!
|
||||
--退后一步是人生
|
||||
|
||||
阅读排行榜
|
||||
|
||||
1. eclipse配置maven + 创建maven项目(三)(14634)
|
||||
2. 在java中使用MongoDB数据库(4427)
|
||||
3. MySQL 经典面试题(3299)
|
||||
4. maven web 项目中启动报错 Java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet(2843)
|
||||
5. 自己把jar包添加到maven仓库中(2157)
|
||||
|
||||
评论排行榜
|
||||
|
||||
1. 如何高效学习读书笔记(7)
|
||||
2. 在java中使用MongoDB数据库(2)
|
||||
3. Maven下载、安装和配置(二)(1)
|
||||
4. MongoDB的备份和部署 高级功能索引,聚合复制,分片(1)
|
||||
|
||||
推荐排行榜
|
||||
|
||||
1. eclipse配置maven + 创建maven项目(三)(2)
|
||||
2. MongoDB 分布式架构 复制 分片 适用性范围(1)
|
||||
3. 将eclipse左边目录结构改为 树形结构(1)
|
||||
4. 如何高效学习读书笔记(1)
|
||||
5. MongoDB分片原理篇(1)
|
||||
|
||||
|
||||
|
||||
Powered by:
|
||||
博客园
|
||||
Copyright © 王守昌
|
@ -1,13 +0,0 @@
|
||||
面试准备:主要准备的知识包括,C/C++、TCP/IP、HTTP、操作系统、数据结构和算法、熟悉Linux操作、熟悉Linux网络编程、数据库
|
||||
(主要是MySql,Redis,Memcached)。
|
||||
|
||||
C/C++必看书籍:C++ Primer、深度探索C++对象模型、STL源码剖析、还有其它一些经典书籍,比如effective系列,侯捷翻译的其它经典
|
||||
TCP/IP必看书籍:TCP/IP详解 卷1:协议, 图解TCP/IP
|
||||
HTTP必看书籍:图解HTTP,HTTP权威指南(这本书我就看了一点)
|
||||
操作系统必看书籍:现代操作系统
|
||||
数据结构和算法必看书籍:数据结构与算法分析C++描述
|
||||
Linux知识必看书籍:鸟哥的Linux私房菜基础
|
||||
Linux编程必看书籍:APUE,UNP。
|
||||
数据库必看书籍:MySql入门很简单,高性能MySql
|
||||
|
||||
链接:http://blog.csdn.net/linux_ever/article/details/53575251
|
@ -1 +0,0 @@
|
||||
- [牛客网:如何准备校招技术面试](https://www.nowcoder.com/discuss/50990?type=0&order=0&pos=14&page=0)
|
@ -1,92 +0,0 @@
|
||||
# 一面
|
||||
## 问有没有做过JAVA Web的项目,选一个讲
|
||||
答:选择了一个做的CMS来讲,因为我是这个项目的总负责人
|
||||
|
||||
|
||||
## 问项目上有没有遇到什么困难
|
||||
答:没有。。
|
||||
## 技术选型是如何决定的。
|
||||
答:感觉不是真让回答技术选型的过程。我就对比了SpringMVC和struts的优缺点,Mybatis和Hibernate的区别。
|
||||
然后他又问了为什么选择了shiro而不是spring security。我就说因为之前没有接触过权限管理的框架,在网上看帖子说spring security比shiro更为复杂和庞大,对于我们项目没有很多的时间和学习成本去接触spring security,shiro更容易上手。
|
||||
|
||||
|
||||
## 数据库查询
|
||||
三个表,其中一个表是其他两个表的关系表。问如何根据一个表的字段查找出另外一张表的字段。
|
||||
回答出sql。
|
||||
我回答了子查询,其实用join更好点,但是一时间不知道怎么join。
|
||||
|
||||
|
||||
## 数据库索引有了解吗?索引的优缺点。
|
||||
|
||||
|
||||
查询快,插入慢。所以适合查询为主的字段。
|
||||
|
||||
|
||||
## 常用的集合有哪些
|
||||
LinkedList,ArrayList,Hashmap。
|
||||
|
||||
|
||||
## LinkedList和ArrayList的区别
|
||||
底层实现不同。LinkedList双向链表,ArrayList数组。ArrayList插入慢,但是查询快。LinkedList插入快,查询慢。
|
||||
|
||||
|
||||
## LinkedList的底层实现是怎么样的?假如往LinkedList的第k个位置插入数据,时间复杂度?最后一个位置插入呢?
|
||||
双向链表。
|
||||
第k个位置插入数据:k+1 ,遍历k位,插入一次。
|
||||
最后一个位置:1。因为是双向链表。
|
||||
|
||||
|
||||
## Hashmap能不能用对象做key。
|
||||
可以。但是得重写hashcode方法和equals方法。
|
||||
|
||||
|
||||
## Hashmap用对象做key时,put后,又修改了这个对象,会发生什么。
|
||||
我就说那得看你设计的这个hashcode和equals方法跟你修改得属性有没有关系,假如有,那会导致put进去,get不出来
|
||||
|
||||
|
||||
## java的异常分类
|
||||
|
||||
|
||||
Error、Exception,还有一个比较特别的RuntimeException
|
||||
|
||||
|
||||
## Java如何捕获异常
|
||||
回答:try...catch..
|
||||
然后问finally不是吗?
|
||||
回答:不是。finally只是配合使用的,没有异常的时候单独使用finally也是可以的。
|
||||
然后问 什么情况下会在finally里面写代码?
|
||||
回答:举了数据库连接的例子。。
|
||||
|
||||
|
||||
## JVM何时会发生内存泄露
|
||||
不知道。。可以看这个帖子
|
||||
http://blog.csdn.net/anxpp/article/details/51325838
|
||||
|
||||
|
||||
看了这个帖子。。估计面试官是想问我数据库连接也会导致内存泄露吧。。。
|
||||
## JVM的内存划分是如何的
|
||||
堆,栈,方法区,程序计数器什么呢。
|
||||
|
||||
|
||||
## 方法区都存放了哪些东西
|
||||
类的信息,静态类和静态变量。
|
||||
|
||||
|
||||
|
||||
|
||||
## 平常私下会做什么事
|
||||
讲了我github一直在维护的个人项目。爬虫和tinyjvm。
|
||||
|
||||
|
||||
## 算法题:0~n共n+1个数,从中随机抽取一个数,剩下的n个数都知道是什么,问随机抽出的数是什么。
|
||||
|
||||
|
||||
刚开始回答了0~n加起来,然后减去剩下的n个数之和,就可以得到了。
|
||||
他问假如n很大会发生什么情况?
|
||||
我说会超出int或者long的最大值。
|
||||
然后选择了bitmap的方式来解决,一个数对应一个bit。
|
||||
#二面
|
||||
聊项目
|
||||
#HR面
|
||||
聊性格
|
||||
|
@ -1,36 +0,0 @@
|
||||
# 一面
|
||||
## 开闭原则
|
||||
## 简单工厂和工厂模式的区别
|
||||
## MySQL的隔离级别
|
||||
## MySQL的索引,原理
|
||||
## MySQL幻读和脏读的区别
|
||||
## Hashmap是不是线程安全的。
|
||||
## Hashmap的底层机制。除了拉链法,还有哪些解决冲突的方法。
|
||||
## static的用法
|
||||
## sleep和wait的区别
|
||||
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
|
||||
|
||||
|
||||
在调用sleep()方法的过程中,线程不会释放对象锁。
|
||||
|
||||
|
||||
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
|
||||
|
||||
|
||||
获取对象锁进入运行状态。
|
||||
## notify和notifyAll的区别
|
||||
这个问题的主要考点在于锁池和等待池
|
||||
http://blog.csdn.net/emailed/article/details/4689220
|
||||
## spring的底层思想是什么
|
||||
## 为什么会有GC?
|
||||
## JVM的垃圾收集算法,以及年轻代、年老代是如何选择垃圾收集算法
|
||||
新生代 复制算法 Eden、Survivor
|
||||
老年代 标记整理/标记清除
|
||||
|
||||
|
||||
## HTTP是哪一层的?
|
||||
## GET和POST的区别
|
||||
## 类加载的流程
|
||||
## 一次浏览器到服务器端的HTTP请求是怎么样的。
|
||||
https://www.zhihu.com/question/34873227
|
||||
## 怎么加快HTTP请求的速度?
|
@ -1,261 +0,0 @@
|
||||
# 一面
|
||||
## 讲下class文件结构
|
||||
|
||||
|
||||
## import是在class文件哪里存储的
|
||||
|
||||
|
||||
## 类是从哪里加载的
|
||||
bootstrap、ext、user
|
||||
|
||||
|
||||
## 栈指令集和寄存器指令集的区别
|
||||
http://www.voidcn.com/blog/pq258280920/article/p-4274066.html
|
||||
|
||||
|
||||
## GC算法。
|
||||
感觉这个必问的。说了下分代收集。
|
||||
|
||||
|
||||
## Http的action都有哪些
|
||||
我回答了GET、POST、PUT、DELETE、HEAD。我说应该还有,但一时想不起来了。他就没继续问了。
|
||||
TRACE、OPTIONS
|
||||
## http1.0和2.0的区别在哪里
|
||||
我说最大的区别就是安全方面。 然后他说还有呢?
|
||||
|
||||
|
||||
## TCP和UDP的区别
|
||||
|
||||
|
||||
## 线程和进程的区别
|
||||
|
||||
|
||||
## QQ应该用哪一种?
|
||||
我说QQ主要保证消息不丢失。可以采用TCP和UDP混合的。在网络环境差的时候,用TCP保证消息不丢失。网络环境好的时候,可以采用UDP加快速度。
|
||||
|
||||
|
||||
我感觉对于TCP和UDP的理解不是很到位,知乎有个问题
|
||||
https://www.zhihu.com/question/20292749
|
||||
## TCP是怎么保证消息可靠性的?
|
||||
|
||||
|
||||
## Git和SVN的区别。
|
||||
讲了Git的分布式版本库和SVN的集中式版本库。
|
||||
|
||||
|
||||
## MySQL假如查询非常慢,怎么解决?
|
||||
1. 建索引
|
||||
2. 分表。
|
||||
面试官感觉不太够,问还有吗?
|
||||
http://tech.meituan.com/mysql-index.html
|
||||
《高性能MySQL》 查询性能优化
|
||||
## 排序算法都有哪些。每个的时间复杂度都是多少?
|
||||
|
||||
|
||||
## 单链表排序
|
||||
时间复杂度O(nlgn) 空间复杂度O(1)
|
||||
可以使用归并排序来做。
|
||||
http://noalgo.info/388.html
|
||||
## 给两分钟讲下一个你做的最好项目。
|
||||
我讲了税务主题爬虫项目。介绍了爬虫的技术。以及我负责URL去重,正文提取以及后期的重构。
|
||||
然后面试官根据这几部分分别展开问。
|
||||
|
||||
|
||||
## url去重。
|
||||
BloomFilter + 根据文件名去重。
|
||||
讲了下为什么这么做。然后面试官问为什么存文件。我说是因为后期恰好也要建搜索引擎,刚好存文件的时候判断文件是否存在。
|
||||
面试官好像对搜索引擎很感兴趣,于是似乎想问我搜索引擎的知识,我说这块不是我负责的,但是知道一些。面试官说,不是你负责的就算了吧。
|
||||
|
||||
|
||||
## 正文提取的步骤
|
||||
简单说了下所用的哈工大的基于行块密度的正文提取算法,中间也加了常见的杂质过滤,以保证最终结果的纯净。
|
||||
|
||||
|
||||
面试官问了怎么组织这些杂质信息呢?这些信息都放在哪里了呢?
|
||||
我说放在了数据库里面,然后启动之后加载到redis里面。
|
||||
|
||||
|
||||
## redis用过吗?讲下你所知道的redis?
|
||||
讲了下IO多路复用
|
||||
|
||||
|
||||
## 说下IO多路复用。 epoll和select的区别。epoll的水平触发和边缘触发。
|
||||
|
||||
|
||||
## 说下BloomFilter的原理。
|
||||
|
||||
|
||||
这个幸好我寒假有研究。具体可以看我的博客。这块回答的蛮好的。
|
||||
http://www.cyhone.com/2017/02/07/Introduce-to-BloomFilter/
|
||||
|
||||
|
||||
## Hashmap 底层实现
|
||||
讲一遍Hashmap的原理。
|
||||
|
||||
|
||||
## Hashmap 可以存null吗?
|
||||
value可以为null。key只能有一个为null。
|
||||
|
||||
|
||||
## ConcurrenyHashMap的底层?
|
||||
我说没看过源码,但知道他是由分段锁来实现的。也没继续问下去。
|
||||
|
||||
|
||||
## 除了synchronized,还有其他的同步方式?
|
||||
重入锁、读写锁。还有一些java提供的同步类。
|
||||
|
||||
|
||||
## 读写锁。加读锁的时候能不能写?
|
||||
|
||||
|
||||
## Web后端是怎么分层的。为什么这么分。
|
||||
讲了下MVC和后端的service、dao和action。
|
||||
|
||||
|
||||
我说主要是业务的解耦以及可以分工合作。
|
||||
## 讲一下你所知道的docker。
|
||||
我说了下docker和虚拟机的区别,主要区别是docker是基于宿主机的内核,而虚拟机是有自己的操作系统内核。
|
||||
主要相似性就是都提供了运行环境的隔离。
|
||||
http://dockone.io/article/723
|
||||
|
||||
|
||||
然后面试官就根据这个点,深入问了一个问题。
|
||||
## 宿主机能不能看到docker的进程
|
||||
我说不能看到。其实是可以看到的!
|
||||
http://dockone.io/question/529
|
||||
|
||||
|
||||
## NOSQL和SQL的区别,以及每个的应用场景
|
||||
|
||||
|
||||
# 二面
|
||||
|
||||
|
||||
## StringBuffer和StringBuilder的区别
|
||||
StringBuilder 线程不安全
|
||||
## Java中final的用处
|
||||
1. 用于类 不可继承
|
||||
2. 用于方法 不可重写
|
||||
3. 用于属性 值不可修改,对象不可修改地址
|
||||
|
||||
|
||||
## 什么情况下不用final会编译失败
|
||||
这个没回答上来。下来查了一下
|
||||
1. 匿名内部类来自外部闭包环境的自由变量必须是final的
|
||||
2. 在外部类成员方法内部的内部类。
|
||||
3. 在一个代码块block里的内部类。
|
||||
https://www.zhihu.com/question/21395848
|
||||
|
||||
|
||||
## 说下HashMap的底层实现
|
||||
这个目前所有面试中都问了。 所以HashMap的源码一定要好好看。
|
||||
## Java8解决冲突的方式
|
||||
我说还是拉链法,似乎不是很满意,然后我说,当超过冲突某个值时,Java8会把链表改为Treemap。
|
||||
## ThreadLocal了解多少
|
||||
讲了下ThreadLocal的源码实现,thread的localmap对象之类的。
|
||||
## 快排的最大时间复杂度、最小时间复杂度、平均时间复杂度。
|
||||
## 快排是不是稳定的。
|
||||
快排是不稳定的。
|
||||
这个真是自己找死,我又说了堆排序是稳定的。实际上是不稳定啊脑子啊脑子。。
|
||||
http://baike.baidu.com/item/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E7%A8%B3%E5%AE%9A%E6%80%A7
|
||||
|
||||
|
||||
## 1000万个数据的Top K
|
||||
说了最大堆。然后让我计算1000万个int有多大,然后说内存足够。
|
||||
然后我讲了下如何用快排实现topk的过程。
|
||||
当内存足够的情况下,用快排时间求topk复杂度可以为O(n)
|
||||
## TCP的四次挥手
|
||||
## UDP一次最大发送多少个字节
|
||||
## Http状态码
|
||||
主要问了302、404、500
|
||||
http://seo.ibw.cn/display.asp?id=82
|
||||
|
||||
## redis和memcached的区别
|
||||
给了个选择题。实际上就是问redis和memcached的差异,最大的差异就是memcached不支持复杂类型如list等。
|
||||
## 最快的进程间通信方式是怎么样的
|
||||
|
||||
|
||||
## linux命令
|
||||
df -h 查看磁盘空间
|
||||
du -h 查看文件夹大小
|
||||
lsof 查看端口占用
|
||||
top | grep 查看cpu占用
|
||||
ps -aux | grep
|
||||
|
||||
|
||||
linux如何查看一个进程的执行路径?
|
||||
http://lovesoo.org/view-processes-running-linux-full-path-method.html
|
||||
ll /proc/{pid}
|
||||
|
||||
通过进程名查找进程PID可以通过 pidof
|
||||
## 逻辑题。
|
||||
五辆汽车加油问题。
|
||||
1/5+1/4+1/3+1/2+1
|
||||
## 问了一个多分布式消费者请求生产者的问题
|
||||
当时一时间没想出来。就往raft协议和一致性hash上面说
|
||||
## raft协议、一致性hash
|
||||
扯了一波主节点分发,领导者选举,一致性hash啥的。我都不知道我在说啥了,估计面试官也晕晕的。。
|
||||
raft协议说完后,面试官问如果没有主控节点,所有节点都是平级的应该怎么如何设计。
|
||||
我说应该加分布式锁。
|
||||
## 如何实现一个分布式锁。
|
||||
我说使用redis可以实现分布式锁,但是具体的怎么实践还不是很清楚。
|
||||
具体的分布式锁的细节,都在此博客中有
|
||||
http://ifeve.com/redis-lock/
|
||||
|
||||
|
||||
最后面试官介绍说他们是腾讯手机QQ团队
|
||||
|
||||
|
||||
前面两面电面面试完之后,HR通知我去深圳面试,当天有三面和四面。
|
||||
# 三面
|
||||
手写代码。
|
||||
1. 两个字符串前n位忽略大小写的比较。
|
||||
2. bitmap实现。
|
||||
|
||||
|
||||
手写完代码之后面试官直接带我去四面,四面是总监面。
|
||||
# 四面
|
||||
|
||||
|
||||
1. 英语六级
|
||||
我的六级是飘过,面试官对此略不满意。
|
||||
2. linux熟悉吗.
|
||||
我说了下/proc.面试官让我讲下底层实现。。
|
||||
然后面试官问linux下的inode熟悉吗。
|
||||
我依稀记得是apue里面讲过,但一点也想不起来了。就说了不知道。
|
||||
|
||||
|
||||
3. TCP为什么四次挥手。
|
||||
4. TIME_WAIT阶段是干嘛的。
|
||||
5. “六度空间理论”该如何表示
|
||||
我说了下用一个巨大的图,然后将其转化为各个节点之间的最短路径问题。
|
||||
6. 链表倒数第k个怎么快速找。
|
||||
双指针方法
|
||||
7. Mac os的发展史
|
||||
8. nio
|
||||
9. 二叉树遍历,非递归的方式
|
||||
|
||||
|
||||
四面我个人总体感觉还不错,但是面试完后在腾讯大厦等了一下午也没HR面,因为要回武汉就没继续等下去,后来通知结果跪了。
|
||||
想想四面走来最终最后跪了,也是蛮遗憾。但是也发现了个人在很多方面的不足。
|
||||
|
||||
|
||||
|
||||
|
||||
后来发现竟然是因为提前走了被认定为自动放弃。。。。好气。。
|
||||
后来参加了腾讯在武汉的校招。到了现场后发现自己被调到了运营开发岗。
|
||||
# 一面(运营开发)
|
||||
## 从浏览器到服务器请求结果,整个流程
|
||||
|
||||
|
||||
面试完之后面试官通知我过了一面。看面试官心情好,就申请了转岗。
|
||||
晚上6点多的时候,被TEG的捞了起来。
|
||||
|
||||
|
||||
# 一面
|
||||
|
||||
|
||||
# 二面
|
||||
|
||||
|
||||
# HR面
|
@ -1,147 +0,0 @@
|
||||
# 简历筛选面
|
||||
|
||||
|
||||
## 介绍下自己
|
||||
|
||||
|
||||
## 介绍下爬虫项目
|
||||
|
||||
|
||||
## 爬虫项目正文提取的算法
|
||||
介绍了下正文提取算法。面试官没继续问下去。。
|
||||
|
||||
|
||||
## 说下你们的爬虫跟其他爬虫比主要不一样的地方是哪里
|
||||
|
||||
|
||||
## 爬虫里面simhash是怎么回事
|
||||
介绍了下simhash是干嘛的。面试官也没继续问下去。。
|
||||
|
||||
|
||||
## 会Java吗。。介绍下垃圾回收方面的东西。
|
||||
回答了分代收集的流程
|
||||
## 会算法吗。。说下常见的排序。。
|
||||
|
||||
|
||||
## 说下冒泡排序的优势。。
|
||||
|
||||
|
||||
## 假如有1000万个数据需要排序,用哪个合适。
|
||||
说的堆排序,这个说错了。实际上应该是二路归并排序,有点遗憾。
|
||||
|
||||
|
||||
## 说下重载和重写。。
|
||||
|
||||
|
||||
## 说下Hashmap
|
||||
没太明白让我说什么。。就把Hashmap的流程说了下。然后问我key和value是怎么存的。。有点懵逼,不知道想问啥,就说了Entry。
|
||||
|
||||
|
||||
总体面试的很不舒服。过了一个多星期接到一个电话告诉我之前的那个是简历筛选面。然后约了面试的时间。
|
||||
|
||||
|
||||
# 一面(38分钟)
|
||||
|
||||
|
||||
## 问了学了那些课?数据结构考了多少分?专业排名怎么样
|
||||
|
||||
|
||||
## 都会哪些语言
|
||||
Java、Golang、Python、PHP。
|
||||
其中Java用的最多。
|
||||
|
||||
|
||||
## 怎么学习Java的?都看了哪些书?英文版的还是中文版的。
|
||||
看书+实践
|
||||
《Think in Java》、《Effective Java》、《Java并发编程指南》、《深入理解Java虚拟机》
|
||||
一般是以中文版为主,中间有歧义的句子会对照着英文版的看。
|
||||
|
||||
|
||||
## Java内存回收
|
||||
分代收集那一套讲一遍。
|
||||
|
||||
|
||||
## 并发。
|
||||
线程间通信方式。
|
||||
ThreadLocal原理、场景。
|
||||
volatile原理:Java内存模型划分为主内存工作内存之类的。
|
||||
|
||||
|
||||
## ARP协议。
|
||||
IP地址->物理地址。
|
||||
|
||||
|
||||
## 有没有做过路由器相关的开发。
|
||||
实际上计算机网络实验课还是弄过的。。但是全忘光了。。只能说没有。。
|
||||
|
||||
|
||||
## 递归的优点与缺点,递归优化。
|
||||
|
||||
|
||||
通过使用尾递归,编译器可以将递归代码优化为非递归的。
|
||||
http://blog.csdn.net/whinah/article/details/6419680
|
||||
http://www.ruanyifeng.com/blog/2015/04/tail-call.html
|
||||
|
||||
|
||||
## 排序算法,了解哪些排序算法,那些是稳定的。
|
||||
|
||||
|
||||
## MySQL索引方面
|
||||
查询一条语句的时候,怎么知道他用了哪些索引。
|
||||
explain
|
||||
show index from
|
||||
## 常见的Linux命令。用的哪个发行版。
|
||||
ps、lsof、df、du、top
|
||||
|
||||
|
||||
## Redis和Memcached的区别。
|
||||
|
||||
|
||||
## 说下项目
|
||||
|
||||
|
||||
# 二面
|
||||
|
||||
|
||||
## 数学建模比赛的情况。
|
||||
## 项目
|
||||
## 线程和进程的区别
|
||||
## 对java线程的理解
|
||||
java内存模型
|
||||
## hashmap与ConcurrentHashMap
|
||||
## 线程的几种状态,状态之间是怎么轮转的
|
||||
## ThreadLocal应用场景
|
||||
## mybatis与jdbc template
|
||||
## limit字段使用
|
||||
|
||||
|
||||
# 三面
|
||||
|
||||
|
||||
## 项目
|
||||
爬虫去重是怎么做的?
|
||||
正文提取是怎么做的。
|
||||
只可以抓取静态页面吗?如果要抓取js渲染的页面应该怎么做。
|
||||
可以抓取Https的网站吗?
|
||||
|
||||
|
||||
## 毕设
|
||||
内容相似度是怎么比较的。
|
||||
余弦相似度的流程。
|
||||
|
||||
|
||||
## 用户体验你怎么看?
|
||||
回答这个问题时候有点懵。。
|
||||
语言组织有点乱。就说了
|
||||
用户UI和用户交互
|
||||
功能不要太深,层次在3-4层就可以。
|
||||
用户行为日志分析
|
||||
用户反馈
|
||||
|
||||
|
||||
## 还报了哪些公司?
|
||||
|
||||
|
||||
这个面试总共十五分钟。。面试官说看了前两面的面试记录,觉得没太多问题要问我的了。
|
||||
心情愉悦。。
|
||||
|
@ -1,6 +0,0 @@
|
||||
IO多路复用
|
||||
Java HashMap LinkedList CurrentHashMap 源码 1 2 3 4 5
|
||||
Java 并发:countDownLatch CAS
|
||||
浏览器访问网站全过程
|
||||
C++ 基础语法
|
||||
sql注入
|
@ -1 +0,0 @@
|
||||
- [牛客网:腾讯后台开发一面二面纪实](https://www.nowcoder.com/discuss/5082)
|
@ -1 +0,0 @@
|
||||
|
@ -1,119 +0,0 @@
|
||||
# 算法
|
||||
|
||||
- 九九乘法表(美团)
|
||||
- 大文件小内存如何排序,大概的思路是大文件划分为小文件然后排序归并。(网易)
|
||||
- 100亿个long int,取中位数(腾富博)
|
||||
- 一个矩阵中元素分类的算法。一个矩阵中的元素和它周围八个元素是相邻的。如果两个元素相邻切相等就认为他们是一类。(微软)
|
||||
- 给2TB数据,2GB的内存,详细说明外部排序算法,算法复杂度。然后回答一共读了多少数据量,写了多少数据。(2路,多路都问了)
|
||||
|
||||
# Java
|
||||
|
||||
- String,StringBuffer,StringBuider区别,结合线程和单例模式变换着问(美团)
|
||||
- JVM垃圾回收,老年和新生代区别(美团)
|
||||
- JVM内存分配(美团)
|
||||
- HashMap原理,寻找第一个entry位置(美团)
|
||||
- Wait()和sleep()区别(58到家)
|
||||
- 线程安全集合类(58到家)
|
||||
- 不加锁,如何实现线程安全(58到家)
|
||||
- String源码看过么?(58到家)
|
||||
- Volatile有什么功能,能否实现线程安全(58到家)
|
||||
- 线程池的四种创建方式,以及线程池的好处(猎聘)
|
||||
- HashMap底层原理,扩容机制(猎聘)
|
||||
- 如何组织列对象序列化,关键字是什么(猎聘)
|
||||
- GC是不是守护线程(盖亚互娱)
|
||||
- 阻塞和非阻塞有什么区别(Rokid)
|
||||
- ArrayList()的扩容机制(随行付)
|
||||
- 如何实现多线程异步方式(Keep)
|
||||
- Sychronized和Lock锁的区别(Keep)
|
||||
- 堆和栈结构(Keep)
|
||||
- 堆一般存放那几种对象(Keep)
|
||||
- Thread和Runnable区别
|
||||
- 字节流和字符流区别(猎豹)
|
||||
- Arraylist和Linkedlis源码(猎豹)
|
||||
|
||||
# Java EE
|
||||
|
||||
- 对Mybatis的理解,使用的好处(Rokid)
|
||||
- 说说Mybatis优点,缓冲,与Hibernate有什么区别
|
||||
- Mybatis的映射原理是什么(Rokid)
|
||||
- 说说BIO,AIO,NIO的区别(Rokid)
|
||||
- Spring和springMVC的处理异常问题(Keep)
|
||||
|
||||
# C++
|
||||
|
||||
- 动态链接库的认识(360)
|
||||
- 与 Java 的区别
|
||||
- vector 动态增长
|
||||
- Socket 通信
|
||||
|
||||
# 计算机网络
|
||||
|
||||
- tcp udp区别,报文格式(网易)
|
||||
- 网络粘包问题及解决方案。(cvte)
|
||||
|
||||
# 操作系统
|
||||
|
||||
- 为什么用epoll,不用select,区别是什么(腾富博)
|
||||
- write和fwrite区别 (58)
|
||||
|
||||
# 数据库
|
||||
|
||||
- 数据库隔离级别,分别产生的错误(美团)
|
||||
- 数据库分页的关键字是什么,如何实现(猎聘)
|
||||
- 数据库中分表的原因(随行付)
|
||||
- Mysql索引原理,怎么实现,符合索引条件(Keep)
|
||||
- 为什么串行化消耗的资源大?(网易)
|
||||
- mysql和Oracle的区别(美团)
|
||||
- 你的项目中给用户密码进行了salt加密,如果加密算法泄漏了怎么办?(美团)
|
||||
|
||||
# Linux
|
||||
|
||||
- 如何查看连接本主机的其他ip(Rokid)
|
||||
- netstat如何查看端口使用情况,参数是什么(Rokid)
|
||||
- Linux如何进行文件删除(神州信息)
|
||||
|
||||
|
||||
# 分布式
|
||||
|
||||
- 问我了解分布式吗(网易)
|
||||
- MapReduce
|
||||
- Hadoop
|
||||
|
||||
# 设计模式
|
||||
|
||||
- 了解的设计模式,结合 jdk(随行付)
|
||||
|
||||
# 面向对象
|
||||
|
||||
- 多态的理解,细说了重载和重写(Keep)
|
||||
|
||||
# 系统设计
|
||||
|
||||
- 设计一个类似于qq的东西,客户端和服务端要怎么设计(网易)
|
||||
|
||||
# 智力题
|
||||
|
||||
- a,b,aa,ab,ba,bb ,,,,,问接下来的是什么(画了一个二叉树,根节点空,左右子树a,b以此类推),然后又改造了一下,换成01,这样移动或加减1就可解决了(Keep)
|
||||
|
||||
- 有足够量的2分、5分、1分硬币,如果想凑齐一元钱,可以有多少种方法(猎豹)
|
||||
|
||||
# 其它
|
||||
|
||||
- 平时爱看什么非技术书(美团)
|
||||
- 学习规划(猎聘)
|
||||
- 群面,开放性思维题(Rokid)
|
||||
- 家庭情况(Keep)
|
||||
- 平时爱好(Keep)
|
||||
- 自身优势(第一:我自学能力不错,做事能坚持,以我的计算机知识为例。第二:我有明确的目标和规划,不会在考研工作之前徘徊。)(网易)
|
||||
- 宁做鸡头不做凤尾 这句话你怎么看?(网易)
|
||||
- 你周围的人怎么评价你?(网易)
|
||||
- 你的自学过程是怎样的?(网易)
|
||||
- 有没女朋友(网易)
|
||||
- 问成绩,问爱好,家在哪,有没有男朋友,工作意向地点是哪(58)
|
||||
- 有没有其他offer(招银)
|
||||
- 对加班怎么看(招银)
|
||||
- 你更希望从事哪方面的工作,应用型还是研究型的工作?为什么?
|
||||
- 你觉得应用和研究之间的关系和区别?
|
||||
- 平时会逛技术论坛吗?
|
||||
- 平时会自己写一些小程序吗?
|
||||
- 来一段英文的自我介绍。
|
@ -1,7 +0,0 @@
|
||||
# [Leetcode刷题五遍还没offer - 听我分析为什么找工作光刷题没用](http://blog.sina.com.cn/s/blog_686de9d90102w0rf.html)
|
||||
|
||||
- 面试官希望你跟他沟通,面试有来有往,上来二话不说闷头就写代码最糟糕。
|
||||
|
||||
- 有些时候,面试官甚至把题目做点小变化,或者是有意无意把题目说模糊点,让你想当然的以为是某种情况,一头跳进去,折腾一番发现跟想象的不一样,结果就是稳稳的拒掉你了。
|
||||
|
||||
- 解决方法很简单:每次做题之前,跟对方迅速说下你的思路,看看是否是他想听到的方法,再动手,就避免问题了。
|
@ -1,6 +0,0 @@
|
||||
|
||||
HTTP缓存机制(cache-control、Expires之类的一系列请求与相应报头字段)
|
||||
|
||||
HTTP1.0和HTTP1.1区别
|
||||
|
||||
session和cookie的区别,禁用cookie后怎么办
|
@ -1,144 +0,0 @@
|
||||
硬链接和软连接区别
|
||||
kill用法,某个进程杀不掉的原因(进入内核态,忽略kill信号)
|
||||
linux用过的命令
|
||||
系统管理命令(如查看内存使用、网络情况)
|
||||
管道的使用 |
|
||||
grep的使用,一定要掌握,每次都会问在文件中查找
|
||||
shell脚本
|
||||
find命令
|
||||
awk使用
|
||||
|
||||
作者:oscarwin
|
||||
链接:https://www.nowcoder.com/discuss/59394
|
||||
来源:牛客网
|
||||
|
||||
进程与线程
|
||||
|
||||
(1) 进程与线程区别?
|
||||
(2) 线程比进程具有哪些优势?
|
||||
(3) 什么时候用多进程?什么时候用多线程?
|
||||
(4) LINUX中进程和线程使用的几个函数?
|
||||
(5) 线程同步?
|
||||
在Windows下线程同步的方式有:互斥量,信号量,事件,关键代码段
|
||||
在Linux下线程同步的方式有:互斥锁,自旋锁,读写锁,屏障(并发完成同一项任务时,屏障的作用特别好使)
|
||||
知道这些锁之间的区别,使用场景?
|
||||
进程间通讯方式
|
||||
|
||||
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
|
||||
|
||||
命名管道 (FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
|
||||
|
||||
信号量:信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据,有XSI信号量和POSIX信号量,POSIX信号量更加完善。
|
||||
|
||||
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
|
||||
|
||||
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。(原理一定要清楚,常考)
|
||||
|
||||
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生,常见的信号。
|
||||
|
||||
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
|
||||
|
||||
匿名管道与命名管道的区别:匿名管道只能在具有公共祖先的两个进程间使用。
|
||||
共享文件映射mmap
|
||||
mmap建立进程空间到文件的映射,在建立的时候并不直接将文件拷贝到物理内存,同样采用缺页终端。mmap映射一个具体的文件可以实现任意进程间共享内存,映射一个匿名文件,可以实现父子进程间共享内存。
|
||||
常见的信号有哪些?:SIGINT,SIGKILL(不能被捕获),SIGTERM(可以被捕获),SIGSEGV,SIGCHLD,SIGALRM
|
||||
内存管理
|
||||
|
||||
1. 虚拟内存的作用?
|
||||
2. 虚拟内存的实现?
|
||||
3. 操作系统层面对内存的管理?
|
||||
4. 内存池的作用?STL里内存池如何实现?
|
||||
5. 进程空间和内核空间对内存的管理不同?
|
||||
6. Linux的slab层,VAM?
|
||||
7. 伙伴算法
|
||||
8. 高端内存
|
||||
进程调度
|
||||
|
||||
1. Linux进程分为两种,实时进程和非实时进程;
|
||||
2. 优先级分为静态优先级和动态优先级,优先级的范围;
|
||||
3. 调度策略,FIFO,LRU,时间片轮转
|
||||
4. 交互进程通过平均睡眠时间而被奖励;
|
||||
死锁
|
||||
|
||||
(1) 死锁产生的条件;
|
||||
(2) 死锁的避免;
|
||||
命令行
|
||||
|
||||
Linux命令 在一个文件中,倒序打印第二行前100个大写字母
|
||||
```
|
||||
cat filename | head -n 2 | tail -n 1 | grep '[[:upper:]]' -o | tr -d '\n'| cut -c 1-100 | rev
|
||||
```
|
||||
|
||||
与CPU,内存,磁盘相关的命令(top,free, df, fdisk)
|
||||
网络相关的命令netstat,tcpdump等
|
||||
sed, awk, grep三个超强大的命名,分别用与格式化修改,统计,和正则查找
|
||||
ipcs和ipcrm命令
|
||||
查找当前目录以及字母下以.c结尾的文件,且文件中包含"hello world"的文件的路径
|
||||
创建定时任务
|
||||
IO模型
|
||||
|
||||
五种IO模型:阻塞IO,非阻塞IO,IO复用,信号驱动式IO,异步IO
|
||||
select,poll,epoll的区别
|
||||
select:是最初解决IO阻塞问题的方法。用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理,如果没有返回
|
||||
存在的问题:
|
||||
1. 内置数组的形式使得select的最大文件数受限与FD_SIZE;
|
||||
2. 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
|
||||
3. 轮寻排查当文件描述符个数很多时,效率很低; poll:通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。poll解决了select重复初始化的问题。轮寻排查的问题未解决。
|
||||
epoll:轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
|
||||
为什么使用IO多路复用,最主要的原因是什么?
|
||||
epoll有两种触发模式?这两种触发模式有什么区别?编程的时候有什么区别?
|
||||
上一题中编程的时候有什么区别,是在边缘触发的时候要把套接字中的数据读干净,那么当有多个套接字时,在读的套接字一直不停的有数据到达,如何保证其他套接字不被饿死(面试网易游戏的时候问的一个问题,答不上来,印象贼深刻)。
|
||||
1. select/poll/epoll区别
|
||||
2. 几种网络服务器模型的介绍与比较
|
||||
3. epoll为什么这么快(搞懂这篇文章,关于IO复用的问题就信手拈来了)
|
||||
线程池
|
||||
|
||||
Linux的API
|
||||
|
||||
fork与vfork区别
|
||||
fork和vfork都用于创建子进程。但是vfork创建子进程后,父进程阻塞,直到子进程调用exit()或者excle()。
|
||||
对于内核中过程fork通过调用clone函数,然后clone函数调用do_fork()。do_fork()中调用copy_process()函数先复制task_struct结构体,然后复制其他关于内存,文件,寄存器等信息。fork采用写时拷贝技术,因此子进程和父进程的页表指向相同的页框。但是vfork不需要拷贝页表,因为父进程会一直阻塞,直接使用父进程页表。
|
||||
exit()与_exit()区别
|
||||
exit()清理后进入内核,_exit()直接陷入内核。
|
||||
孤儿进程与僵死进程
|
||||
1. 孤儿进程是怎么产生的?
|
||||
2. 僵死进程是怎么产生的?
|
||||
3. 僵死进程的危害?
|
||||
4. 如何避免僵死进程的产生?
|
||||
Linux是如何避免内存碎片的
|
||||
1. 伙伴算法,用于管理物理内存,避免内存碎片;
|
||||
2. 高速缓存Slab层用于管理内核分配内存,避免碎片。
|
||||
共享内存的实现原理?
|
||||
共享内存实现分为两种方式一种是采用mmap,另一种是采用XSI机制中的共享内存方法。mmap是内存文件映射,将一个文件映射到进程的地址空间,用户进程的地址空间的管理是通过vm_area_struct结构体进行管理的。mmap通过映射一个相同的文件到两个不同的进程,就能实现这两个进程的通信,采用该方法可以实现任意进程之间的通信。mmap也可以采用匿名映射,不指定映射的文件,但是只能在父子进程间通信。XSI的内存共享实际上也是通过映射文件实现,只是其映射的是一种特殊文件系统下的文件,该文件是不能通过read和write访问的。
|
||||
二者区别:
|
||||
1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。
|
||||
2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。
|
||||
3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。
|
||||
|
||||
系统调用与库函数(open, close, create, lseek, write, read)
|
||||
同步方法有哪些?
|
||||
1. 互斥锁,自旋锁,信号量,读写锁,屏障
|
||||
2. 互斥锁与自旋锁的区别:互斥锁得不到资源的时候阻塞,不占用cpu资源。自旋锁得不到资源的时候,不停的查询,而然占用cpu资源。
|
||||
3. 死锁
|
||||
其他
|
||||
|
||||
++i是否是原子操作
|
||||
明显不是,++i主要有三个步骤,把数据从内存放在寄存器上,在寄存器上进行自增,把数据从寄存器拷贝会内存,每个步骤都可能被中断。
|
||||
判断大小端
|
||||
|
||||
```
|
||||
union un
|
||||
{
|
||||
int i;
|
||||
char ch;
|
||||
};
|
||||
void fun()
|
||||
{
|
||||
union un test;
|
||||
test.i = 1;
|
||||
if(ch == 1)
|
||||
cout << "小端" << endl;
|
||||
else
|
||||
cout << "大端" << endl;
|
||||
}
|
||||
```
|
@ -1 +0,0 @@
|
||||
- [最近5年133个Java面试问题列表](http://www.importnew.com/17232.html)
|
@ -1 +0,0 @@
|
||||
https://www.jianshu.com/p/977a9e7d80b3
|
@ -1,2 +0,0 @@
|
||||
http://wdxtub.com/interview/14520847747820.html
|
||||
https://zhuanlan.zhihu.com/p/23755202
|
@ -1 +0,0 @@
|
||||
[计算机网络方面的面试基础题](https://wenku.baidu.com/view/6ad2acc0aa00b52acfc7ca21.html)
|
Loading…
x
Reference in New Issue
Block a user