I write protocol messages encoder/decoder for my project.
All messages have 2-bytes header (1 byte - message type, 1 byte message sub-type) and body.
There are two types of messages:

1. Plain message - no encoding
2. Encoded message - AES encoding

I would like to get your feedback on:

1. Overall design and coding style
2. Proposals for performance (it is very important) and memory usage
improvement.

Comment #1: the following code is working code.
Comment #2: the "Unsafe" stream classes are classes that extend ByteArray streams. The aim is to get direct access to internal stream buffer (it is protected).

```
public class PacketBuilder {

    /**
     * Creates DatagramPacket. The packet data is encoded.
     * The cipher should be initialized with Cipher.ENCRYPT_MODE.
     * 
     * @param data the data to be sent
     * @param cipher the cipher
     * @param sendTo the destination address
     * 
     * @return DatagramPacket
     * 
     * @throws NullPointerException if any of arguments is null
     * @throws EncodingException if error occurred during encoding
     */
    public static DatagramPacket buildEncoded(byte[] data, Cipher cipher, SocketAddress sendTo) 
    throws NullPointerException, EncodingException {

        Objects.requireNonNull(data);
        Objects.requireNonNull(cipher);
        Objects.requireNonNull(sendTo);

        try {

            UnsafeByteArrayOutputStream bytes = new UnsafeByteArrayOutputStream();
            CipherOutputStream cipherOutput = new CipherOutputStream(bytes, cipher);

            cipherOutput.write(data);
            cipherOutput.flush();
            cipherOutput.close();

            DatagramPacket packet = new DatagramPacket(bytes.getBuffer(), bytes.getByteCount());
            packet.setSocketAddress(sendTo);
            return packet;

        } catch (IllegalStateException | IOException e) {
            throw new EncodingException(e);
        }
    }

    /**
     * Creates DatagramPacket. The packet data is encoded.
     * The cipher should be initialized with Cipher.ENCRYPT_MODE.
     * 
     * @param type the message type
     * @param subType the message sub-type
     * @param string the string to be sent
     * @param cipher the cipher
     * @param sendTo the destination address
     * 
     * @return DatagramPacket
     * 
     * @throws NullPointerException if any of arguments is null
     * @throws EncodingException if error occurred during encoding  
     */
    public static DatagramPacket buildEncoded(byte type, byte subType, String string, Cipher cipher, SocketAddress sendTo)
    throws NullPointerException, EncodingException {

        Objects.requireNonNull(string);
        Objects.requireNonNull(cipher);
        Objects.requireNonNull(sendTo);

        byte[] data = null;

        try {
            data = string.getBytes(StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e1) {};

        try {

            UnsafeByteArrayOutputStream bytes = new UnsafeByteArrayOutputStream();
            CipherOutputStream cipherOutput = new CipherOutputStream(bytes, cipher);

            cipherOutput.write(type);
            cipherOutput.write(subType);
            cipherOutput.write(data);
            cipherOutput.flush();
            cipherOutput.close();

            DatagramPacket packet = new DatagramPacket(bytes.getBuffer(), bytes.getByteCount());
            packet.setSocketAddress(sendTo);
            return packet;

        } catch (IllegalStateException | IOException e) {
            throw new EncodingException(e);
        }
    }

    /**
     * Creates DatagramPacket. The packet data is not encoded
     *  
     * @param type the message type
     * @param subType the message sub-type
     * @param data the data to be sent
     * @param sendTo the destination address
     * 
     * @return DatagramPacket  
     * 
     * @throws NullPointerException if any of arguments is null
     */
    public static DatagramPacket buildPlain(byte type, byte subType, String string, SocketAddress sendTo) 
    throws NullPointerException {

        Objects.requireNonNull(string);
        Objects.requireNonNull(sendTo);

        byte[] data = null;

        try {
            data = string.getBytes(StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e1) {};

        UnsafeByteArrayOutputStream out = new UnsafeByteArrayOutputStream();

        try {
            out.write(type);
            out.write(subType);
            out.write(data);
            out.flush();
            out.close();
        } catch (IOException e) {}; //we cannot be here 

        DatagramPacket packet = new DatagramPacket(out.getBuffer(), out.getByteCount());
        packet.setSocketAddress(sendTo);
        return packet;
    }

public class PacketParser {

    /**
     * Decode packet data and creates message.  
     * The cipher have to be initialized with Cipher.DECRYPT_MODE
     * 
     * @param packet the packet to convert to message
     * @param cipher the cipher
     * 
     * @return message
     * 
     * @throws NullPointerException if any of arguments is null
     * @throws IllegalMessageException if packet data cannot be converted to message
     * @throws DecodingException if error occurred during decoding
     */
    public static Message parseEncoded(DatagramPacket packet,  Cipher cipher) 
    throws NullPointerException, IllegalMessageException, DecodingException {

        validate(packet);
        Objects.requireNonNull(cipher);

        UnsafeByteArrayInputStream bytesInput;
        CipherInputStream cipherInput = null;
        UnsafeByteArrayOutputStream bytesOutput = null;

        try {

            bytesInput = new UnsafeByteArrayInputStream(packet.getData(), 0, packet.getLength());
            cipherInput = new CipherInputStream(bytesInput, cipher);

            bytesOutput = new UnsafeByteArrayOutputStream();

            for (int b = cipherInput.read(); b > -1; b = cipherInput.read())
                bytesOutput.write(b);

            return new Message(bytesOutput.getBuffer(), bytesOutput.getByteCount(),  packet.getSocketAddress());

        } catch (IllegalStateException | IOException e) {
            throw new DecodingException(e);

        } finally {
            try {
                if (cipherInput != null) cipherInput.close();
                if (bytesOutput != null) bytesOutput.close();
            } catch (Exception e) {};
        }   
    }

    /**
     * Creates message from the given packet 
     * 
     * @param packet the packet to convert to message
     * 
     * @return message
     * 
     * @throws NullPointerException if any of arguments is null
     * @throws IllegalMessageException if packet data cannot be converted to message
     */
    public static Message parsePlain(DatagramPacket packet) 
    throws NullPointerException, IllegalMessageException  {

        validate(packet);

        return new Message(packet.getData(), packet.getLength(),  packet.getSocketAddress());

    }

    /*
     * Validates the packet. Throws exception if 
     * message has no header or header is illegal
     */
    private static void validate(DatagramPacket packet) 
    throws NullPointerException,  IllegalMessageException {

        Objects.requireNonNull(packet);

        if (packet.getLength() < HEADER_LENGTH)
            throw new IllegalMessageException("Message too short");

        int type = packet.getData()[0];
        int subType = packet.getData()[1];

        switch (type) {

        case Constants.REGISTRATION:

            switch (subType) {
            case Constants.HELLO:
            case Constants.KEY_EXCHANGE:
            case Constants.CLIENT_IF:
            case Constants.ACCESS:
            case Constants.ERROR:
                break;
            default:
                throw new IllegalMessageException("Unknown message");
            }

            break;

        case Constants.HEART_BEAT:
        case Constants.CONN_REQUEST:
            break;
        default: 
            throw new IllegalMessageException("Unknown message");

        }
    }

    public static class Message {

        final private byte type;
        final private byte subType;

        final private byte[] data;
        final private int length;

        final private SocketAddress address;

        private Message (byte[] buffer, int length, SocketAddress address) {

            this.type = buffer[0];
            this.subType = buffer[1];
            this.address = address;

            byte[] data = new byte[length - HEADER_LENGTH];
            System.arraycopy(buffer, HEADER_LENGTH, data, 0, data.length);

            this.data = data;
            this.length = data.length;
        }

        public byte getType() {
            return type;
        }

        public byte getSubType() {
            return subType;
        }

        public byte[] getData() {
            return data;
        }

        public int getLength() {
            return length;
        }

        public SocketAddress getAddress() {
            return address;
        }

    }
}

```